From 3be137a8d80d1b3ec086de03429d030e78f8d213 Mon Sep 17 00:00:00 2001 From: e-minguez Date: Thu, 3 Oct 2024 17:40:18 +0200 Subject: [PATCH] October 2024 revamp - Replaced Bird by native MetalLB - Setup Ingress IP properly - Deploy an extra Ingress to handle the Global IP (instead of abusing the regular Ingress that makes unable to expose cluster-wide apps and only global ones) - Added Rancher deployment (Rancher flavor and version can be provided) - K3s or RKE2 (any version can be specified) - Added a helper script to perform the Rancher bootstrap process automatically as well as importing all the clusters to it - Added `shfmt` to the pre-commit action - Switched to Terraform >= 1.9.0 (as required to include a new validation) - Fixed pre-commit action -> https://github.com/pre-commit/action/issues/210 Closes #53, closes #81, closes #67, closes #66 --- .github/workflows/pre-commit.yaml | 7 +- README.md | 356 ++++++--- examples/demo_cluster/README.md | 12 +- examples/demo_cluster/clusters-to-rancher.sh | 118 +++ examples/demo_cluster/outputs.tf | 2 +- .../demo_cluster/terraform.tfvars.example | 23 +- examples/demo_cluster/variables.tf | 17 +- main.tf | 17 +- modules/k3s_cluster/outputs.tf | 4 - modules/k3s_cluster/templates/user-data.tftpl | 388 ---------- .../{k3s_cluster => kube_cluster}/README.md | 46 +- modules/{k3s_cluster => kube_cluster}/main.tf | 74 +- modules/kube_cluster/outputs.tf | 34 + .../kube_cluster/templates/user-data.tftpl | 707 ++++++++++++++++++ .../variables.tf | 44 +- .../{k3s_cluster => kube_cluster}/versions.tf | 0 outputs.tf | 22 +- rancher-clusters-imported.png | Bin 0 -> 131856 bytes variables.tf | 21 +- versions.tf | 2 +- 20 files changed, 1315 insertions(+), 579 deletions(-) create mode 100755 examples/demo_cluster/clusters-to-rancher.sh delete mode 100644 modules/k3s_cluster/outputs.tf delete mode 100644 modules/k3s_cluster/templates/user-data.tftpl rename modules/{k3s_cluster => kube_cluster}/README.md (62%) rename modules/{k3s_cluster => kube_cluster}/main.tf (68%) create mode 100644 modules/kube_cluster/outputs.tf create mode 100644 modules/kube_cluster/templates/user-data.tftpl rename modules/{k3s_cluster => kube_cluster}/variables.tf (57%) rename modules/{k3s_cluster => kube_cluster}/versions.tf (100%) create mode 100644 rancher-clusters-imported.png diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 669555c..10ad4a2 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -19,7 +19,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - tf: [1.3.0] + tf: [1.9.0] tflint: [v0.44.1] permissions: pull-requests: write @@ -31,6 +31,8 @@ jobs: - name: Install Python3 uses: actions/setup-python@v5 + with: + python-version: '3.x' - name: Install tflint uses: terraform-linters/setup-tflint@v4 @@ -69,4 +71,7 @@ jobs: platform: linux arch: amd64 + - name: Install shfmt + uses: mfinelli/setup-shfmt@v3 + - uses: pre-commit/action@v3.0.1 diff --git a/README.md b/README.md index 47d5455..108abbf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# K3s on Equinix Metal +# K3s/RKE2 on Equinix Metal [![GitHub release](https://img.shields.io/github/release/equinix-labs/terraform-equinix-metal-k3s/all.svg?style=flat-square)](https://github.com/equinix-labs/terraform-equinix-metal-k3s/releases) ![](https://img.shields.io/badge/Stability-Experimental-red.svg) @@ -8,43 +8,46 @@
Table of content

- * [Introduction](#introduction) - * [Prerequisites](#prerequisites) - * [Variable requirements](#variable-requirements) - * [Demo application](#demo-application) - * [Notes](#notes) - * [Example scenarios](#example-scenarios) - * [Single node in default Metro](#single-node-in-default-metros) - * [Single node in 2 different Metros](#single-node-in-2-different-metros) - * [1 x HA cluster with 3 nodes & 4 public IPs + 2 x Single Node cluster (same Metro), a Global IPV4 and the demo app deployed](#1-x-ha-cluster-with-3-nodes--4-public-ips--2-x-single-node-cluster-same-metro-a-global-ipv4-and-the-demo-app-deployed) - * [Usage](#usage) - * [Accessing the clusters](#accessing-the-clusters) - * [Terraform module documentation](#terraform-module-documentation) - * [Requirements](#requirements-1) - * [Providers](#providers) - * [Modules](#modules) - * [Resources](#resources) - * [Inputs](#inputs) - * [Outputs](#outputs) - * [Contributing](#contributing) - * [License](#license) +- [K3s/RKE2 on Equinix Metal](#k3srke2-on-equinix-metal) + - [Table of content](#table-of-content) + - [Introduction](#introduction) + - [Prerequisites](#prerequisites) + - [Variable requirements](#variable-requirements) + - [Demo application](#demo-application) + - [Example scenarios](#example-scenarios) + - [Single node in default Metro](#single-node-in-default-metro) + - [Single node in 2 different Metros](#single-node-in-2-different-metros) + - [1 x All-in-one cluster with Rancher (stable), a custom K3s version \& 1 public IP (+1 for Ingress) + 1 x All-in-one with 1 extra node \& a custom RKE2 version + 1 x HA cluster with 3 nodes \& 4 public IPs. Global IPV4 and demo app deployed](#1-x-all-in-one-cluster-with-rancher-stable-a-custom-k3s-version--1-public-ip-1-for-ingress--1-x-all-in-one-with-1-extra-node--a-custom-rke2-version--1-x-ha-cluster-with-3-nodes--4-public-ips-global-ipv4-and-demo-app-deployed) + - [Usage](#usage) + - [Accessing the clusters](#accessing-the-clusters) + - [Rancher bootstrap and add all clusters to Rancher](#rancher-bootstrap-and-add-all-clusters-to-rancher) + - [Terraform module documentation](#terraform-module-documentation) + - [Requirements](#requirements) + - [Providers](#providers) + - [Modules](#modules) + - [Resources](#resources) + - [Inputs](#inputs) + - [Outputs](#outputs) + - [Contributing](#contributing) + - [License](#license)

## Introduction -This is a [Terraform](hhttps://registry.terraform.io/providers/equinix/metal/latest/docs) project for deploying [K3s](https://k3s.io) on [Equinix Metal](https://metal.equinix.com) intended to allow you to quickly spin-up and down K3s clusters. +This is a [Terraform](hhttps://registry.terraform.io/providers/equinix/metal/latest/docs) project for deploying [K3s](https://k3s.io) (or [RKE2](https://docs.rke2.io/)) on [Equinix Metal](https://metal.equinix.com) intended to allow you to quickly spin-up and down K3s/RKE2 clusters. -[K3s](https://docs.k3s.io/) is a fully compliant and lightweight Kubernetes distribution focused on Edge, IoT, ARM or just for situations where a PhD in K8s clusterology is infeasible. +[K3s](https://docs.k3s.io/) is a fully compliant and lightweight Kubernetes distribution focused on Edge, IoT, ARM or just for situations where a PhD in K8s clusterology is infeasible. [RKE2](https://docs.rke2.io/) is Rancher’s next-generation Kubernetes distribution, it combines the best-of-both-worlds from the 1.x version of RKE (hereafter referred to as RKE1) anxd K3s. From K3s, it inherits the usability, ease-of-operations, and deployment model. From RKE1, it inherits close alignment with upstream Kubernetes. In places K3s has diverged from upstream Kubernetes in order to optimize for edge deployments, but RKE1 and RKE2 can stay closely aligned with upstream. > :warning: This repository is [Experimental](https://github.com/packethost/standards/blob/master/experimental-statement.md) meaning that it's based on untested ideas or techniques and not yet established or finalized or involves a radically new and innovative style! This means that support is best effort (at best!) and we strongly encourage you to NOT use this in production. This terraform project supports a wide variety of scenarios and mostly focused on Edge, such as: -* Single node K3s cluster on a single Equinix Metal Metro. -* HA K3s cluster (3 control plane nodes) using BGP to provide an HA K3s API entrypoint. +* Single node K3s/RKE2 cluster on a single Equinix Metal Metro. +* HA K3s/RKE2 cluster (3 control plane nodes) using [MetalLB](https://metallb.universe.tf/) + BGP to provide an HA K3s/RKE2 API entrypoint. * Any number of worker nodes (both for single node or HA scenarios). -* Any number of public IPv4s to be used to expose services to the outside using `LoadBalancer` services via [MetalLB](https://metallb.universe.tf/) (deployed automatically). +* Any number of public IPv4s to be used to expose services to the outside using `LoadBalancer` services via MetalLB. +* Optionally it can deploy Rancher Manager on top of the cluster. * All those previous scenarios but deploying multiple clusters on multiple Equinix Metal metros. * A Global IPv4 that is shared in all cluster among all Equnix Metal Metros and can be used to expose an example application to demonstrate load balancing between different Equinix Metal Metros. @@ -89,13 +92,13 @@ There is a lot of flexibility in the module to allow customization of the differ |------|-------------|------|---------|:--------:| | [metal\_auth\_token](#input\_metal\_auth\_token) | Your Equinix Metal API key | `string` | n/a | yes | | [metal\_project\_id](#input\_metal\_project\_id) | Your Equinix Metal Project ID | `string` | n/a | yes | -| [clusters](#input\_clusters) | K3s cluster definition | `list of K3s cluster objects` | n/a | yes | +| [clusters](#input\_clusters) | Kubernetes cluster definition | `list of kubernetes cluster objects` | n/a | yes | > :note: The Equinix Metal Auth Token should be defined in a `provider` block in your own Terraform config. In this project, that is done in `examples/demo_cluster/`, not in the root. This pattern facilitates [Implicit Provider Inheritance](https://developer.hashicorp.com/terraform/language/modules/develop/providers#implicit-provider-inheritance) and better reuse of Terraform modules. For more details on the variables, see the [Terraform module documentation](#terraform-module-documentation) section. -The default variables are set to deploy a single node K3s cluster in the FR Metro, using a Equinix Metal's c3.small.x86. You just need to add the cluster name as: +The default variables are set to deploy a single node K3s (latest K3s version available) cluster in the FR Metro, using a Equinix Metal's c3.small.x86. You just need to add the cluster name as: ```bash metal_auth_token = "redacted" @@ -107,7 +110,7 @@ clusters = [ ] ``` -Change each default variable at your own risk, see [Example scenarios](#example-scenarios) and the [K3s module README.md file](modules/k3s_cluster/README.md) for more details. +Change each default variable at your own risk, see [Example scenarios](#example-scenarios) and the [kube_cluster module README.md file](modules/kube_cluster/README.md) for more details. > :warning: The hostnames are created based on the Cluster Name and the `control_plane_hostnames` & `node_hostnames` variables (normalized), beware the lenght of those variables. @@ -117,7 +120,7 @@ You can create a [terraform.tfvars](https://developer.hashicorp.com/terraform/la ## Demo application -If enabled (`deploy_demo = true`), a demo application ([hello-kubernetes](https://github.com/paulbouwer/hello-kubernetes)) will be deployed on all the clusters. The Global IPv4 will be used by the [K3s Traefik Ingress Controller](https://docs.k3s.io/networking#traefik-ingress-controller) to expose that application and the load will be spreaded among all the clusters. This means that different requests will be routed to different clusters. See [the MetalLB documentation](https://metallb.universe.tf/concepts/bgp/#load-balancing-behavior) for more information about how BGP load balancing works. +If enabled (`deploy_demo = true`), a demo application ([hello-kubernetes](https://github.com/paulbouwer/hello-kubernetes)) will be deployed on all the clusters. An extra [Ingress-NGINX Controller](https://github.com/kubernetes/ingress-nginx) is deployed on each cluster to expose that application and the load will be spreaded among all the clusters. This means that different requests will be routed to different clusters. See [the MetalLB documentation](https://metallb.universe.tf/concepts/bgp/#load-balancing-behavior) for more information about how BGP load balancing works. ## Example scenarios @@ -138,8 +141,18 @@ This will produce something similar to: ```bash Outputs: -k3s_api = { - "FR DEV Cluster" = "145.40.94.83" +clusters_output = { + "cluster_details" = { + "FR DEV Cluster" = { + "api" = "147.28.184.239" + "nodes" = { + "fr-dev-cluster-cp-aio" = { + "node_private_ipv4" = "10.25.49.1" + "node_public_ipv4" = "147.28.184.239" + } + } + } + } } ``` @@ -164,36 +177,59 @@ This will produce something similar to: ```bash Outputs: -k3s_api = { - "FR DEV Cluster" = "145.40.94.83", - "SV DEV Cluster" = "86.109.11.205" +clusters_output = { + "cluster_details" = { + "FR DEV Cluster" = { + "api" = "147.28.184.239" + "nodes" = { + "fr-dev-cluster-cp-aio" = { + "node_private_ipv4" = "10.25.49.1" + "node_public_ipv4" = "147.28.184.239" + } + } + } + "SV DEV Cluster" = { + "api" = "139.178.70.53" + "nodes" = { + "sv-dev-cluster-cp-aio" = { + "node_private_ipv4" = "10.67.31.129" + "node_public_ipv4" = "139.178.70.53" + } + } + } + } } ``` -### 1 x HA cluster with 3 nodes & 4 public IPs + 2 x Single Node cluster (same Metro), a Global IPV4 and the demo app deployed +### 1 x All-in-one cluster with Rancher (stable), a custom K3s version & 1 public IP (+1 for Ingress) + 1 x All-in-one with 1 extra node & a custom RKE2 version + 1 x HA cluster with 3 nodes & 4 public IPs. Global IPV4 and demo app deployed ```bash metal_auth_token = "redacted" metal_project_id = "redacted" -clusters = [{ - name = "SV Production" - ip_pool_count = 4 - k3s_ha = true - metro = "SV" - node_count = 3 -}, -{ - name = "FR Dev 1" - metro = "FR" -}, -{ - name = "FR Dev 2" - metro = "FR" -} +clusters = [ + { + name = "FR DEV Cluster" + rancher_flavor = "stable" + ip_pool_count = 1 + kube_version = "v1.29.9+k3s1" + }, + { + name = "SV DEV Cluster" + metro = "SV" + node_count = 1 + kube_version = "v1.30.3+rke2r1" + }, + { + name = "SV Production" + ip_pool_count = 4 + ha = true + metro = "SV" + node_count = 3 + } ] -global_ip = true -deploy_demo = true +global_ip = true +deploy_demo = true ``` This will produce something similar to: @@ -201,12 +237,72 @@ This will produce something similar to: ```bash Outputs: -anycast_ip = "147.75.40.52" -demo_url = "http://hellok3s.147.75.40.52.sslip.io" -k3s_api = { - "FR Dev 1" = "145.40.94.83", - "FR Dev 2" = "147.75.192.250", - "SV Production" = "86.109.11.205" +clusters_output = { + "anycast_ip" = "147.75.40.34" + "cluster_details" = { + "FR DEV Cluster" = { + "api" = "147.28.184.239" + "ingress" = "147.28.184.119" + "ip_pool_cidr" = "147.28.184.118/32" + "nodes" = { + "fr-dev-cluster-cp-aio" = { + "node_private_ipv4" = "10.25.49.1" + "node_public_ipv4" = "147.28.184.239" + } + } + } + "SV DEV Cluster" = { + "api" = "139.178.70.53" + "nodes" = { + "sv-dev-cluster-cp-aio" = { + "node_private_ipv4" = "10.67.31.129" + "node_public_ipv4" = "139.178.70.53" + } + "sv-dev-cluster-node-00" = { + "node_private_ipv4" = "10.67.31.131" + "node_public_ipv4" = "86.109.11.115" + } + } + } + "SV Production" = { + "api" = "86.109.11.239" + "ingress" = "86.109.11.53" + "ip_pool_cidr" = "139.178.70.68/30" + "nodes" = { + "sv-production-cp-0" = { + "node_private_ipv4" = "10.67.31.133" + "node_public_ipv4" = "139.178.70.141" + } + "sv-production-cp-1" = { + "node_private_ipv4" = "10.67.31.137" + "node_public_ipv4" = "136.144.54.109" + } + "sv-production-cp-2" = { + "node_private_ipv4" = "10.67.31.143" + "node_public_ipv4" = "139.178.94.11" + } + "sv-production-node-00" = { + "node_private_ipv4" = "10.67.31.141" + "node_public_ipv4" = "136.144.54.113" + } + "sv-production-node-01" = { + "node_private_ipv4" = "10.67.31.135" + "node_public_ipv4" = "139.178.70.233" + } + "sv-production-node-02" = { + "node_private_ipv4" = "10.67.31.139" + "node_public_ipv4" = "136.144.54.111" + } + } + } + } + "demo_url" = "http://hellok3s.147.75.40.34.sslip.io" + "rancher_urls" = { + "FR DEV Cluster" = { + "rancher_initial_password_base64" = "Zm9vdmFsdWU=" + "rancher_url" = "https://rancher.147.28.184.119.sslip.io" + } + } } ``` @@ -251,8 +347,18 @@ Apply complete! Resources: 3 added, 0 changed, 0 destroyed. Outputs: -k3s_api = { - "FR example" = "145.40.94.83" +clusters_output = { + "cluster_details" = { + "FR DEV Cluster" = { + "api" = "147.28.184.239" + "nodes" = { + "fr-dev-cluster-cp-aio" = { + "node_private_ipv4" = "10.25.49.1" + "node_public_ipv4" = "147.28.184.239" + } + } + } + } } ``` @@ -262,86 +368,141 @@ As the SSH key for the project has been injected, the clusters can be accessed a ```bash ( -MODULENAME="demo_cluster" +OUTPUT=$(terraform output -json) IFS=$'\n' -for cluster in $(terraform output -json | jq -r ".${MODULENAME}.value.k3s_api | keys[]"); do - IP=$(terraform output -json | jq -r ".${MODULENAME}.value.k3s_api[\"${cluster}\"]") - ssh -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${IP} kubectl get nodes +for cluster in $(echo ${OUTPUT} | jq -r ".clusters_output.value.cluster_details | keys[]"); do + FIRSTHOST=$(echo ${OUTPUT} | jq -r "first(.clusters_output.value.cluster_details[\"${cluster}\"].nodes[].node_public_ipv4)") + echo "=== ${cluster} ===" + ssh -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${FIRSTHOST} -tt 'bash -l -c "kubectl get nodes -o wide"' done ) -NAME STATUS ROLES AGE VERSION -ny-k3s-aio Ready control-plane,master 9m35s v1.26.5+k3s1 -NAME STATUS ROLES AGE VERSION -sv-k3s-aio Ready control-plane,master 10m v1.26.5+k3s +=== FR DEV Cluster === +NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME +fr-dev-cluster-cp-aio Ready control-plane,master 4m31s v1.29.9+k3s1 10.25.49.1 147.28.184.239 Debian GNU/Linux 11 (bullseye) 5.10.0-32-amd64 containerd://1.7.21-k3s2 +=== SV DEV Cluster === +NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME +sv-dev-cluster-cp-aio Ready control-plane,etcd,master 4m3s v1.30.3+rke2r1 10.67.31.129 139.178.70.53 Debian GNU/Linux 11 (bullseye) 5.10.0-32-amd64 containerd://1.7.17-k3s1 +sv-dev-cluster-node-00 Ready 2m29s v1.30.3+rke2r1 10.67.31.133 139.178.70.233 Debian GNU/Linux 11 (bullseye) 5.10.0-32-amd64 containerd://1.7.17-k3s1 +=== SV Production === +NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME +sv-production-cp-0 Ready control-plane,etcd,master 2m46s v1.30.5+k3s1 10.67.31.131 139.178.70.141 Debian GNU/Linux 11 (bullseye) 5.10.0-32-amd64 containerd://1.7.21-k3s2 +sv-production-cp-1 Ready control-plane,etcd,master 42s v1.30.5+k3s1 10.67.31.137 136.144.54.111 Debian GNU/Linux 11 (bullseye) 5.10.0-32-amd64 containerd://1.7.21-k3s2 +sv-production-cp-2 Ready control-plane,etcd,master 26s v1.30.5+k3s1 10.67.31.139 136.144.54.113 Debian GNU/Linux 11 (bullseye) 5.10.0-32-amd64 containerd://1.7.21-k3s2 +sv-production-node-00 Ready 63s v1.30.5+k3s1 10.67.31.135 136.144.54.109 Debian GNU/Linux 11 (bullseye) 5.10.0-32-amd64 containerd://1.7.21-k3s2 +sv-production-node-01 Ready 59s v1.30.5+k3s1 10.67.31.141 139.178.94.11 Debian GNU/Linux 11 (bullseye) 5.10.0-32-amd64 containerd://1.7.21-k3s2 +sv-production-node-02 Ready 57s v1.30.5+k3s1 10.67.31.143 139.178.94.19 Debian GNU/Linux 11 (bullseye) 5.10.0-32-amd64 containerd://1.7.21-k3s2 ``` -To access from outside, the K3s kubeconfig file can be copied to any host and replace the `server` field with the IP of the K3s API: +To access from outside, the kubeconfig file can be copied to any host and replace the `server` field with the IP of the kubernetes API: ```bash ( -MODULENAME="demo_cluster" +OUTPUT=$(terraform output -json) IFS=$'\n' -for cluster in $(terraform output -json | jq -r ".${MODULENAME}.value.k3s_api | keys[]"); do - IP=$(terraform output -json | jq -r ".${MODULENAME}.value.k3s_api[\"${cluster}\"]") - export KUBECONFIG="./$(echo ${cluster}| tr -c -s '[:alnum:]' '-')-kubeconfig" - scp -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${IP}:/etc/rancher/k3s/k3s.yaml ${KUBECONFIG} - sed -i "s/127.0.0.1/${IP}/g" ${KUBECONFIG} +for cluster in $(echo ${OUTPUT} | jq -r ".clusters_output.value.cluster_details | keys[]"); do + FIRSTHOST=$(echo ${OUTPUT} | jq -r "first(.clusters_output.value.cluster_details[\"${cluster}\"].nodes[].node_public_ipv4)") + API=$(echo ${OUTPUT} | jq -r ".clusters_output.value.cluster_details[\"${cluster}\"].api") + export KUBECONFIG="./$(echo ${cluster}| tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 ]/-/g' | sed 's/ /-/g' | sed 's/^-*\|-*$/''/g')-kubeconfig" + scp -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${FIRSTHOST}:/root/.kube/config ${KUBECONFIG} + sed -i "s/127.0.0.1/${API}/g" ${KUBECONFIG} chmod 600 ${KUBECONFIG} + echo "=== ${cluster} ===" kubectl get nodes done ) -NAME STATUS ROLES AGE VERSION -ny-k3s-aio Ready control-plane,master 8m41s v1.26.5+k3s1 -NAME STATUS ROLES AGE VERSION -sv-k3s-aio Ready control-plane,master 9m20s v1.26.5+k3s1 +=== FR DEV Cluster === +NAME STATUS ROLES AGE VERSION +fr-dev-cluster-cp-aio Ready control-plane,master 10m v1.29.9+k3s1 +=== SV DEV Cluster === +NAME STATUS ROLES AGE VERSION +sv-dev-cluster-cp-aio Ready control-plane,etcd,master 10m v1.30.3+rke2r1 +sv-dev-cluster-node-00 Ready 8m43s v1.30.3+rke2r1 +=== SV Production === +NAME STATUS ROLES AGE VERSION +sv-production-cp-0 Ready control-plane,etcd,master 9m v1.30.5+k3s1 +sv-production-cp-1 Ready control-plane,etcd,master 6m56s v1.30.5+k3s1 +sv-production-cp-2 Ready control-plane,etcd,master 6m40s v1.30.5+k3s1 +sv-production-node-00 Ready 7m17s v1.30.5+k3s1 +sv-production-node-01 Ready 7m13s v1.30.5+k3s1 +sv-production-node-02 Ready 7m11s v1.30.5+k3s1 ``` -> :warning: OSX sed is different, it needs to be used as `sed -i "" "s/127.0.0.1/${IP}/g" ${KUBECONFIG}` instead. +> :warning: OSX sed is different, it needs to be used as `sed -i "" "s/127.0.0.1/${API}/g" ${KUBECONFIG}` instead. ```bash ( -MODULENAME="demo_cluster" +OUTPUT=$(terraform output -json) IFS=$'\n' -for cluster in $(terraform output -json | jq -r ".${MODULENAME}.value.k3s_api | keys[]"); do - IP=$(terraform output -json | jq -r ".${MODULENAME}.value.k3s_api[\"${cluster}\"]") - export KUBECONFIG="./$(echo ${cluster}| tr -c -s '[:alnum:]' '-')-kubeconfig" - scp -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${IP}:/etc/rancher/k3s/k3s.yaml ${KUBECONFIG} - sed -i "" "s/127.0.0.1/${IP}/g" ${KUBECONFIG} +for cluster in $(echo ${OUTPUT} | jq -r ".clusters_output.value.cluster_details | keys[]"); do + FIRSTHOST=$(echo ${OUTPUT} | jq -r "first(.clusters_output.value.cluster_details[\"${cluster}\"].nodes[].node_public_ipv4)") + API=$(echo ${OUTPUT} | jq -r ".clusters_output.value.cluster_details[\"${cluster}\"].api") + export KUBECONFIG="./$(echo ${cluster}| tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 ]/-/g' | sed 's/ /-/g' | sed 's/^-*\|-*$/''/g')-kubeconfig" + scp -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${FIRSTHOST}:/root/.kube/config ${KUBECONFIG} + sed -i "" "s/127.0.0.1/${API}/g" ${KUBECONFIG} chmod 600 ${KUBECONFIG} + echo "=== ${cluster} ===" kubectl get nodes done ) -NAME STATUS ROLES AGE VERSION -ny-k3s-aio Ready control-plane,master 8m41s v1.26.5+k3s1 -NAME STATUS ROLES AGE VERSION -sv-k3s-aio Ready control-plane,master 9m20s v1.26.5+k3s1 +=== FR DEV Cluster === +NAME STATUS ROLES AGE VERSION +fr-dev-cluster-cp-aio Ready control-plane,master 10m v1.29.9+k3s1 +=== SV DEV Cluster === +NAME STATUS ROLES AGE VERSION +sv-dev-cluster-cp-aio Ready control-plane,etcd,master 10m v1.30.3+rke2r1 +sv-dev-cluster-node-00 Ready 8m43s v1.30.3+rke2r1 +=== SV Production === +NAME STATUS ROLES AGE VERSION +sv-production-cp-0 Ready control-plane,etcd,master 9m v1.30.5+k3s1 +sv-production-cp-1 Ready control-plane,etcd,master 6m56s v1.30.5+k3s1 +sv-production-cp-2 Ready control-plane,etcd,master 6m40s v1.30.5+k3s1 +sv-production-node-00 Ready 7m17s v1.30.5+k3s1 +sv-production-node-01 Ready 7m13s v1.30.5+k3s1 +sv-production-node-02 Ready 7m11s v1.30.5+k3s1 ``` +## Rancher bootstrap and add all clusters to Rancher + +There is a helper script [clusters-to-rancher.sh](./examples/demo_cluster/clusters-to-rancher.sh) that will perform the +[Rancher first login process](https://ranchermanager.docs.rancher.com/how-to-guides/new-user-guides/authentication-permissions-and-global-configuration#first-log-in) +automatically based on the Terraform output. + +The script also imports all the other clusters where rancher wasn't deployed. + +It only requires the admin password (>=12 characters) that Rancher will use moving forward: + +```bash +./clusters-to-rancher.sh -p +``` + +![Awesome Rancher screenshot](./rancher-clusters-imported.png?raw=true "Clusters imported") + ## Terraform module documentation + ### Requirements | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.3 | +| [terraform](#requirement\_terraform) | >= 1.9 | | [equinix](#requirement\_equinix) | >= 1.14.2 | ### Providers | Name | Version | |------|---------| -| [equinix](#provider\_equinix) | >= 1.14.2 | +| [equinix](#provider\_equinix) | 1.14.3 | ### Modules | Name | Source | Version | |------|--------|---------| -| [k3s\_cluster](#module\_k3s\_cluster) | ./modules/k3s_cluster | n/a | +| [kube\_cluster](#module\_kube\_cluster) | ./modules/kube_cluster | n/a | ### Resources @@ -353,18 +514,19 @@ sv-k3s-aio Ready control-plane,master 9m20s v1.26.5+k3s1 | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [metal\_project\_id](#input\_metal\_project\_id) | Equinix Metal Project ID | `string` | n/a | yes | -| [clusters](#input\_clusters) | K3s cluster definition |
list(object({
name = optional(string, "K3s demo cluster")
metro = optional(string, "FR")
plan_control_plane = optional(string, "c3.small.x86")
plan_node = optional(string, "c3.small.x86")
node_count = optional(number, 0)
k3s_ha = optional(bool, false)
os = optional(string, "debian_11")
control_plane_hostnames = optional(string, "k3s-cp")
node_hostnames = optional(string, "k3s-node")
custom_k3s_token = optional(string, "")
ip_pool_count = optional(number, 0)
k3s_version = optional(string, "")
metallb_version = optional(string, "")
}))
|
[
{}
]
| no | +| [clusters](#input\_clusters) | Cluster definition |
list(object({
name = optional(string, "Demo cluster")
metro = optional(string, "FR")
plan_control_plane = optional(string, "c3.small.x86")
plan_node = optional(string, "c3.small.x86")
node_count = optional(number, 0)
ha = optional(bool, false)
os = optional(string, "debian_11")
control_plane_hostnames = optional(string, "cp")
node_hostnames = optional(string, "node")
custom_token = optional(string, "")
ip_pool_count = optional(number, 0)
kube_version = optional(string, "")
metallb_version = optional(string, "")
rancher_flavor = optional(string, "")
rancher_version = optional(string, "")
custom_rancher_password = optional(string, "")
}))
|
[
{}
]
| no | | [deploy\_demo](#input\_deploy\_demo) | Deploys a simple demo using a global IP as ingress and a hello-kubernetes pods | `bool` | `false` | no | | [global\_ip](#input\_global\_ip) | Enables a global anycast IPv4 that will be shared for all clusters in all metros | `bool` | `false` | no | +| [metal\_project\_id](#input\_metal\_project\_id) | Equinix Metal Project ID | `string` | n/a | yes | ### Outputs | Name | Description | |------|-------------| | [anycast\_ip](#output\_anycast\_ip) | Global IP shared across Metros | +| [cluster\_details](#output\_cluster\_details) | List of Clusters => K8s details | | [demo\_url](#output\_demo\_url) | URL of the demo application to demonstrate a global IP shared across Metros | -| [k3s\_api](#output\_k3s\_api) | List of Clusters => K3s APIs | +| [rancher\_urls](#output\_rancher\_urls) | List of Clusters => Rancher details | ## Contributing diff --git a/examples/demo_cluster/README.md b/examples/demo_cluster/README.md index 6b5f6ec..3040d1d 100644 --- a/examples/demo_cluster/README.md +++ b/examples/demo_cluster/README.md @@ -1,6 +1,6 @@ -# SiDemo Cluster Example +# Demo Cluster Examples -This example demonstrates usage of the Equinix Metal K3s module. A Demo application is installed. +This example demonstrates usage of the Equinix Metal K3s/RKE2 module. A Demo application is installed. ## Usage @@ -36,15 +36,15 @@ No resources. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [metal\_auth\_token](#input\_metal\_auth\_token) | Your Equinix Metal API key | `string` | n/a | yes | -| [metal\_project\_id](#input\_metal\_project\_id) | Your Equinix Metal Project ID | `string` | n/a | yes | -| [clusters](#input\_clusters) | K3s cluster definition |
list(object({
name = optional(string, "K3s demo cluster")
metro = optional(string, "FR")
plan_control_plane = optional(string, "c3.small.x86")
plan_node = optional(string, "c3.small.x86")
node_count = optional(number, 0)
k3s_ha = optional(bool, false)
os = optional(string, "debian_11")
control_plane_hostnames = optional(string, "k3s-cp")
node_hostnames = optional(string, "k3s-node")
custom_k3s_token = optional(string, "")
ip_pool_count = optional(number, 0)
k3s_version = optional(string, "")
metallb_version = optional(string, "")
}))
|
[
{}
]
| no | +| [clusters](#input\_clusters) | Cluster definition |
list(object({
name = optional(string, "Demo cluster")
metro = optional(string, "FR")
plan_control_plane = optional(string, "c3.small.x86")
plan_node = optional(string, "c3.small.x86")
node_count = optional(number, 0)
ha = optional(bool, false)
os = optional(string, "debian_11")
control_plane_hostnames = optional(string, "cp")
node_hostnames = optional(string, "node")
custom_token = optional(string, "")
ip_pool_count = optional(number, 0)
kube_version = optional(string, "")
metallb_version = optional(string, "")
rancher_version = optional(string, "")
rancher_flavor = optional(string, "")
custom_rancher_password = optional(string, "")
}))
|
[
{}
]
| no | | [deploy\_demo](#input\_deploy\_demo) | Deploys a simple demo using a global IP as ingress and a hello-kubernetes pods | `bool` | `false` | no | | [global\_ip](#input\_global\_ip) | Enables a global anycast IPv4 that will be shared for all clusters in all metros | `bool` | `false` | no | +| [metal\_auth\_token](#input\_metal\_auth\_token) | Your Equinix Metal API key | `string` | n/a | yes | +| [metal\_project\_id](#input\_metal\_project\_id) | Your Equinix Metal Project ID | `string` | n/a | yes | ### Outputs | Name | Description | |------|-------------| -| [demo\_cluster](#output\_demo\_cluster) | Passthrough of the root module output | +| [clusters\_output](#output\_clusters\_output) | Passthrough of the root module output | diff --git a/examples/demo_cluster/clusters-to-rancher.sh b/examples/demo_cluster/clusters-to-rancher.sh new file mode 100755 index 0000000..16bc9fb --- /dev/null +++ b/examples/demo_cluster/clusters-to-rancher.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + echo "Usage: $0 -p " + exit 1 +} + +die() { + echo ${1} 1>&2 + exit ${2} +} + +prechecks() { + command -v kubectl >/dev/null 2>&1 || die "Error: kubectl not found" 1 + command -v curl >/dev/null 2>&1 || die "Error: curl not found" 1 + command -v jq >/dev/null 2>&1 || die "Error: jq not found" 1 + command -v scp >/dev/null 2>&1 || die "Error: scp not found" 1 +} + +wait_for_rancher() { + while ! curl -k "${RANCHERURL}/ping" >/dev/null 2>&1; do sleep 1; done +} + +bootstrap_rancher() { + # Get token + TOKEN=$(curl -sk -X POST ${RANCHERURL}/v3-public/localProviders/local?action=login -H 'content-type: application/json' -d "{\"username\":\"admin\",\"password\":\"${RANCHERPASS}\"}" | jq -r .token) + + # Set password + curl -q -sk ${RANCHERURL}/v3/users?action=changepassword -H 'content-type: application/json' -H "Authorization: Bearer ${TOKEN}" -d "{\"currentPassword\":\"${RANCHERPASS}\",\"newPassword\":\"${PASSWORD}\"}" + + # Create a temporary API token (ttl=60 minutes) + APITOKEN=$(curl -sk ${RANCHERURL}/v3/token -H 'content-type: application/json' -H "Authorization: Bearer ${TOKEN}" -d '{"type":"token","description":"automation","ttl":3600000}' | jq -r .token) + + # Set the Rancher URL + curl -q -sk ${RANCHERURL}/v3/settings/server-url -H 'content-type: application/json' -H "Authorization: Bearer ${APITOKEN}" -X PUT -d "{\"name\":\"server-url\",\"value\":\"${RANCHERURL}\"}" +} + +get_cluster_kubeconfig() { + cluster="${1}" + FIRSTHOST=$(echo ${OUTPUT} | jq -r "first(.clusters_output.value.cluster_details[\"${cluster}\"].nodes[].node_public_ipv4)") + API=$(echo ${OUTPUT} | jq -r ".clusters_output.value.cluster_details[\"${cluster}\"].api") + KUBECONFIG="$(mktemp)" + export KUBECONFIG + scp -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${FIRSTHOST}:/root/.kube/config ${KUBECONFIG} + # Linux + [ "$(uname -o)" == "GNU/Linux" ] && sed -i "s/127.0.0.1/${API}/g" ${KUBECONFIG} + # OSX + [ "$(uname -o)" == "Darwin" ] && sed -i "" "s/127.0.0.1/${API}/g" ${KUBECONFIG} + chmod 600 ${KUBECONFIG} + echo ${KUBECONFIG} +} + +clusters_to_rancher() { + RANCHERKUBE=$(get_cluster_kubeconfig "${RANCHERCLUSTER}") + + IFS=$'\n' + for clustername in ${OTHERCLUSTERS}; do + export KUBECONFIG=${RANCHERKUBE} + normalizedname=$(echo ${clustername} | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 ]/-/g' | sed 's/ /-/g' | sed 's/^-*\|-*$/''/g') + cat <<-EOF | kubectl apply -f - >/dev/null 2>&1 + apiVersion: provisioning.cattle.io/v1 + kind: Cluster + metadata: + name: ${normalizedname} + namespace: fleet-default + spec: {} + EOF + MANIFEST="$(kubectl get clusterregistrationtokens.management.cattle.io -n "$(kubectl get clusters.provisioning.cattle.io -n fleet-default "${normalizedname}" -o jsonpath='{.status.clusterName}')" default-token -o jsonpath='{.status.manifestUrl}')" + DESTKUBECONFIG=$(get_cluster_kubeconfig "${clustername}") + curl --insecure -sfL ${MANIFEST} | kubectl --kubeconfig ${DESTKUBECONFIG} apply -f - >/dev/null 2>&1 + rm -f "${DESTKUBECONFIG}" + done + + rm -f "${RANCHERKUBE}" +} + +PASSWORD="" +while getopts ":p:" opt; do + case $opt in + p) + PASSWORD=$OPTARG + ;; + \?) + echo "Invalid option: -$OPTARG" >&2 + usage + ;; + :) + echo "Option -$OPTARG requires an argument." >&2 + usage + ;; + esac +done + +if [ -z "$PASSWORD" ]; then + echo "Error: Password is required." 1>&2 + usage +fi + +if [ ${#PASSWORD} -lt 12 ]; then + die "Error: Password must be at least 12 characters long." 1 +fi + +[ ! -f "./terraform.tfstate" ] && die "Error: ./terraform.tfstate does not exist." 1 + +OUTPUT=$(terraform output -json) + +[ "${OUTPUT}" == "{}" ] && die "Error. terraform output is '{}'." 1 + +RANCHERCLUSTER=$(echo ${OUTPUT} | jq -r 'first(.clusters_output.value.rancher_urls | keys[])') +RANCHERURL=$(echo ${OUTPUT} | jq -r ".clusters_output.value.rancher_urls[\"${RANCHERCLUSTER}\"].rancher_url") +RANCHERPASS=$(echo ${OUTPUT} | jq -r ".clusters_output.value.rancher_urls[\"${RANCHERCLUSTER}\"].rancher_initial_password_base64" | base64 -d) +OTHERCLUSTERS=$(echo ${OUTPUT} | jq -r ".clusters_output.value.cluster_details | keys[] | select(. != \"${RANCHERCLUSTER}\")") + +prechecks +wait_for_rancher +bootstrap_rancher +clusters_to_rancher diff --git a/examples/demo_cluster/outputs.tf b/examples/demo_cluster/outputs.tf index bcf7ada..8432eb1 100644 --- a/examples/demo_cluster/outputs.tf +++ b/examples/demo_cluster/outputs.tf @@ -1,4 +1,4 @@ -output "demo_cluster" { +output "clusters_output" { description = "Passthrough of the root module output" value = module.demo } diff --git a/examples/demo_cluster/terraform.tfvars.example b/examples/demo_cluster/terraform.tfvars.example index 4c7f268..a374c00 100644 --- a/examples/demo_cluster/terraform.tfvars.example +++ b/examples/demo_cluster/terraform.tfvars.example @@ -1,11 +1,26 @@ metal_auth_token="your_token_here" #This must be a user API token metal_project_id="your_project_id" -clusters = [ +clusters = [ { - name = "Your cluster name" + name = "FR DEV Cluster" + rancher_flavor = "stable" + ip_pool_count = 1 + kube_version = "v1.29.9+k3s1" }, { - name = "Your cluster name" - metro = "SV" + name = "SV DEV Cluster" + metro = "SV" + node_count = 1 + kube_version = "v1.30.3+rke2r1" + }, + { + name = "SV Production" + ip_pool_count = 4 + ha = true + metro = "SV" + node_count = 3 } ] + +global_ip = true +deploy_demo = true diff --git a/examples/demo_cluster/variables.tf b/examples/demo_cluster/variables.tf index 527256d..4bbddb0 100644 --- a/examples/demo_cluster/variables.tf +++ b/examples/demo_cluster/variables.tf @@ -22,21 +22,24 @@ variable "deploy_demo" { } variable "clusters" { - description = "K3s cluster definition" + description = "Cluster definition" type = list(object({ - name = optional(string, "K3s demo cluster") + name = optional(string, "Demo cluster") metro = optional(string, "FR") plan_control_plane = optional(string, "c3.small.x86") plan_node = optional(string, "c3.small.x86") node_count = optional(number, 0) - k3s_ha = optional(bool, false) + ha = optional(bool, false) os = optional(string, "debian_11") - control_plane_hostnames = optional(string, "k3s-cp") - node_hostnames = optional(string, "k3s-node") - custom_k3s_token = optional(string, "") + control_plane_hostnames = optional(string, "cp") + node_hostnames = optional(string, "node") + custom_token = optional(string, "") ip_pool_count = optional(number, 0) - k3s_version = optional(string, "") + kube_version = optional(string, "") metallb_version = optional(string, "") + rancher_version = optional(string, "") + rancher_flavor = optional(string, "") + custom_rancher_password = optional(string, "") })) default = [{}] } diff --git a/main.tf b/main.tf index d64c0e7..49ad487 100644 --- a/main.tf +++ b/main.tf @@ -1,15 +1,13 @@ locals { global_ip_cidr = var.global_ip ? equinix_metal_reserved_ip_block.global_ip[0].cidr_notation : "" - # tflint-ignore: terraform_unused_declarations - validate_demo = (var.deploy_demo == true && var.global_ip == false) ? tobool("Demo is only deployed if global_ip = true.") : true } ################################################################################ -# K3S Cluster In-line Module +# K8s Cluster In-line Module ################################################################################ -module "k3s_cluster" { - source = "./modules/k3s_cluster" +module "kube_cluster" { + source = "./modules/kube_cluster" for_each = { for cluster in var.clusters : cluster.name => cluster } @@ -18,14 +16,17 @@ module "k3s_cluster" { plan_control_plane = each.value.plan_control_plane plan_node = each.value.plan_node node_count = each.value.node_count - k3s_ha = each.value.k3s_ha + ha = each.value.ha os = each.value.os control_plane_hostnames = each.value.control_plane_hostnames node_hostnames = each.value.node_hostnames - custom_k3s_token = each.value.custom_k3s_token - k3s_version = each.value.k3s_version + custom_token = each.value.custom_token + kube_version = each.value.kube_version metallb_version = each.value.metallb_version ip_pool_count = each.value.ip_pool_count + rancher_flavor = each.value.rancher_flavor + rancher_version = each.value.rancher_version + custom_rancher_password = each.value.custom_rancher_password metal_project_id = var.metal_project_id deploy_demo = var.deploy_demo global_ip_cidr = local.global_ip_cidr diff --git a/modules/k3s_cluster/outputs.tf b/modules/k3s_cluster/outputs.tf deleted file mode 100644 index 7e6cb62..0000000 --- a/modules/k3s_cluster/outputs.tf +++ /dev/null @@ -1,4 +0,0 @@ -output "k3s_api_ip" { - value = try(equinix_metal_reserved_ip_block.api_vip_addr[0].address, equinix_metal_device.all_in_one[0].network[0].address) - description = "K3s API IPs" -} diff --git a/modules/k3s_cluster/templates/user-data.tftpl b/modules/k3s_cluster/templates/user-data.tftpl deleted file mode 100644 index 0fb1ff4..0000000 --- a/modules/k3s_cluster/templates/user-data.tftpl +++ /dev/null @@ -1,388 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -wait_for_k3s_api(){ - # Wait for the node to be available, meaning the K8s API is available - while ! kubectl wait --for condition=ready node $(cat /etc/hostname | tr '[:upper:]' '[:lower:]') --timeout=60s; do sleep 2 ; done -} - -install_bird(){ - # Install bird - apt update && apt install bird jq -y - - # In order to configure bird, the metadata information is required. - # BGP info can take a few seconds to be populated, retry if that's the case - INTERNAL_IP="null" - while [ $${INTERNAL_IP} == "null" ]; do - echo "BGP data still not available..." - sleep 5 - METADATA=$(curl -s https://metadata.platformequinix.com/metadata) - INTERNAL_IP=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].customer_ip') - done - PEER_IP_1=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].peer_ips[0]') - PEER_IP_2=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].peer_ips[1]') - ASN=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].customer_as') - ASN_AS=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].peer_as') - MULTIHOP=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].multihop') - GATEWAY=$(echo $${METADATA} | jq -r '.network.addresses[] | select(.public == true and .address_family == 4) | .gateway') - - # Generate the bird configuration based on the metadata values - # https://deploy.equinix.com/developers/guides/configuring-bgp-with-bird/ - cat <<-EOF >/etc/bird/bird.conf - router id $${INTERNAL_IP}; - - protocol direct { - interface "lo"; - } - - protocol kernel { - persist; - scan time 60; - import all; - export all; - } - - protocol device { - scan time 60; - } - - protocol static { - route $${PEER_IP_1}/32 via $${GATEWAY}; - route $${PEER_IP_2}/32 via $${GATEWAY}; - } - - filter metal_bgp { - accept; - } - - protocol bgp neighbor_v4_1 { - export filter metal_bgp; - local as $${ASN}; - multihop; - neighbor $${PEER_IP_1} as $${ASN_AS}; - } - - protocol bgp neighbor_v4_2 { - export filter metal_bgp; - local as $${ASN}; - multihop; - neighbor $${PEER_IP_2} as $${ASN_AS}; - } - EOF - - # Wait for K3s to be up, otherwise the second and third control plane nodes will try to join localhost - wait_for_k3s_api - - # Configure the BGP interface - # https://deploy.equinix.com/developers/guides/configuring-bgp-with-bird/ - if ! grep -q 'lo:0' /etc/network/interfaces; then - cat <<-EOF >>/etc/network/interfaces - - auto lo:0 - iface lo:0 inet static - address ${API_IP} - netmask 255.255.255.255 - EOF - ifup lo:0 - fi - - # Enable IP forward for bird - # TODO: Check if this is done automatically with K3s, it doesn't hurt however - echo "net.ipv4.ip_forward=1" | tee /etc/sysctl.d/99-ip-forward.conf - sysctl --load /etc/sysctl.d/99-ip-forward.conf - - # Debian usually starts the service after being installed, but just in case - systemctl enable bird - systemctl restart bird -} - -install_metallb(){ - apt update && apt install -y curl jq - -%{ if metallb_version != "" ~} - export METALLB_VERSION=${metallb_version} -%{ else ~} - export METALLB_VERSION=$(curl --silent "https://api.github.com/repos/metallb/metallb/releases/latest" | jq -r .tag_name) -%{ endif ~} - - # Wait for K3s to be up. It should be up already but just in case. - wait_for_k3s_api - - # Apply the MetalLB manifest - kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/$${METALLB_VERSION}/config/manifests/metallb-native.yaml - - # Wait for MetalLB to be up - while ! kubectl wait --for condition=ready -n metallb-system $(kubectl get pods -n metallb-system -l component=controller -o name) --timeout=10s; do sleep 2 ; done - - # In order to configure MetalLB, the metadata information is required. - # BGP info can take a few seconds to be populated, retry if that's the case - INTERNAL_IP="null" - while [ $${INTERNAL_IP} == "null" ]; do - echo "BGP data still not available..." - sleep 5 - METADATA=$(curl -s https://metadata.platformequinix.com/metadata) - INTERNAL_IP=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].customer_ip') - done - PEER_IP_1=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].peer_ips[0]') - PEER_IP_2=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].peer_ips[1]') - ASN=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].customer_as') - ASN_AS=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].peer_as') - -%{ if global_ip_cidr != "" ~} - # Configure the IPAddressPool for the Global IP if present - cat <<- EOF | kubectl apply -f - - apiVersion: metallb.io/v1beta1 - kind: IPAddressPool - metadata: - name: anycast-ip - namespace: metallb-system - spec: - addresses: - - ${global_ip_cidr} - autoAssign: false - EOF -%{ endif ~} - -%{ if ip_pool != "" ~} - # Configure the IPAddressPool for the IP pool if present - cat <<- EOF | kubectl apply -f - - apiVersion: metallb.io/v1beta1 - kind: IPAddressPool - metadata: - name: ippool - namespace: metallb-system - spec: - addresses: - - ${ip_pool} - autoAssign: false - EOF -%{ endif ~} - - # Configure the BGPPeer for each peer IP - cat <<- EOF | kubectl apply -f - - apiVersion: metallb.io/v1beta2 - kind: BGPPeer - metadata: - name: equinix-metal-peer-1 - namespace: metallb-system - spec: - peerASN: $${ASN_AS} - myASN: $${ASN} - peerAddress: $${PEER_IP_1} - sourceAddress: $${INTERNAL_IP} - EOF - - cat <<- EOF | kubectl apply -f - - apiVersion: metallb.io/v1beta2 - kind: BGPPeer - metadata: - name: equinix-metal-peer-1 - namespace: metallb-system - spec: - peerASN: $${ASN_AS} - myASN: $${ASN} - peerAddress: $${PEER_IP_2} - sourceAddress: $${INTERNAL_IP} - EOF - - # Enable the BGPAdvertisement, only to be executed in the control-plane nodes - cat <<- EOF | kubectl apply -f - - apiVersion: metallb.io/v1beta1 - kind: BGPAdvertisement - metadata: - name: bgp-peers - namespace: metallb-system - spec: - nodeSelectors: - - matchLabels: - node-role.kubernetes.io/control-plane: "true" - EOF -} - -install_k3s(){ - # Curl is needed to download the k3s binary - # Jq is needed to parse the Equinix Metal metadata (json format) - apt update && apt install curl jq -y - - # Download the K3s installer script - curl -L --output k3s_installer.sh https://get.k3s.io && install -m755 k3s_installer.sh /usr/local/bin/ - -%{ if node_type == "control-plane" ~} - # If the node to be installed is the second or third control plane or extra nodes, wait for the API to be up - # Wait for the first control plane node to be up - while ! curl -m 10 -s -k -o /dev/null https://${API_IP}:6443 ; do echo "API still not reachable"; sleep 2 ; done -%{ endif ~} -%{ if node_type == "node" ~} - # Wait for the first control plane node to be up - while ! curl -m 10 -s -k -o /dev/null https://${API_IP}:6443 ; do echo "API still not reachable"; sleep 2 ; done -%{ endif ~} - - export INSTALL_K3S_SKIP_START=false - export K3S_TOKEN="${k3s_token}" - export NODE_IP=$(curl -s https://metadata.platformequinix.com/metadata | jq -r '.network.addresses[] | select(.public == false and .address_family == 4) |.address') - export NODE_EXTERNAL_IP=$(curl -s https://metadata.platformequinix.com/metadata | jq -r '.network.addresses[] | select(.public == true and .address_family == 4) |.address') -%{ if node_type == "all-in-one" ~} -%{ if global_ip_cidr != "" ~} - export INSTALL_K3S_EXEC="server --write-kubeconfig-mode=644 --disable=servicelb --node-ip $${NODE_IP} --node-external-ip $${NODE_EXTERNAL_IP}" -%{ else ~} -%{ if ip_pool != "" ~} - export INSTALL_K3S_EXEC="server --write-kubeconfig-mode=644 --disable=servicelb --node-ip $${NODE_IP} --node-external-ip $${NODE_EXTERNAL_IP}" -%{ else ~} - export INSTALL_K3S_EXEC="server --write-kubeconfig-mode=644 --node-ip $${NODE_IP} --node-external-ip $${NODE_EXTERNAL_IP}" -%{ endif ~} -%{ endif ~} -%{ endif ~} -%{ if node_type == "control-plane-master" ~} - export INSTALL_K3S_EXEC="server --cluster-init --write-kubeconfig-mode=644 --tls-san=${API_IP} --tls-san=${API_IP}.sslip.io --disable=servicelb --node-ip $${NODE_IP} --node-external-ip $${NODE_EXTERNAL_IP}" -%{ endif ~} -%{ if node_type == "control-plane" ~} - export INSTALL_K3S_EXEC="server --server https://${API_IP}:6443 --write-kubeconfig-mode=644 --node-ip $${NODE_IP} --node-external-ip $${NODE_EXTERNAL_IP}" -%{ endif ~} -%{ if node_type == "node" ~} - export INSTALL_K3S_EXEC="agent --server https://${API_IP}:6443 --node-ip $${NODE_IP} --node-external-ip $${NODE_EXTERNAL_IP}" -%{ endif ~} -%{ if k3s_version != "" ~} - export INSTALL_K3S_VERSION=${k3s_version} -%{ endif ~} - /usr/local/bin/k3s_installer.sh - - systemctl enable --now k3s -} - -deploy_demo(){ - kubectl annotate svc -n kube-system traefik "metallb.universe.tf/address-pool=anycast-ip" - - # I cannot make split work in Terraform templates - IP=$(echo ${global_ip_cidr} | cut -d/ -f1) - cat <<- EOF | kubectl apply -f - - --- - apiVersion: v1 - kind: Namespace - metadata: - name: hello-kubernetes - --- - apiVersion: v1 - kind: ServiceAccount - metadata: - name: hello-kubernetes - namespace: hello-kubernetes - labels: - app.kubernetes.io/name: hello-kubernetes - --- - apiVersion: v1 - kind: Service - metadata: - name: hello-kubernetes - namespace: hello-kubernetes - labels: - app.kubernetes.io/name: hello-kubernetes - spec: - type: ClusterIP - ports: - - port: 80 - targetPort: http - protocol: TCP - name: http - selector: - app.kubernetes.io/name: hello-kubernetes - --- - apiVersion: apps/v1 - kind: Deployment - metadata: - name: hello-kubernetes - namespace: hello-kubernetes - labels: - app.kubernetes.io/name: hello-kubernetes - spec: - replicas: 2 - selector: - matchLabels: - app.kubernetes.io/name: hello-kubernetes - template: - metadata: - labels: - app.kubernetes.io/name: hello-kubernetes - spec: - serviceAccountName: hello-kubernetes - containers: - - name: hello-kubernetes - image: "paulbouwer/hello-kubernetes:1.10" - imagePullPolicy: IfNotPresent - ports: - - name: http - containerPort: 8080 - protocol: TCP - livenessProbe: - httpGet: - path: / - port: http - readinessProbe: - httpGet: - path: / - port: http - env: - - name: HANDLER_PATH_PREFIX - value: "" - - name: RENDER_PATH_PREFIX - value: "" - - name: KUBERNETES_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: KUBERNETES_POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: KUBERNETES_NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - - name: CONTAINER_IMAGE - value: "paulbouwer/hello-kubernetes:1.10" - --- - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - name: hello-kubernetes-ingress - namespace: hello-kubernetes - spec: - rules: - - host: hellok3s.$${IP}.sslip.io - http: - paths: - - path: "/" - pathType: Prefix - backend: - service: - name: hello-kubernetes - port: - name: http - EOF -} - -install_k3s - -%{ if node_type == "control-plane-master" ~} -install_bird -install_metallb -%{ endif ~} -%{ if node_type == "control-plane" ~} -install_bird -install_metallb -%{ endif ~} - -%{ if node_type == "all-in-one" ~} -%{ if global_ip_cidr != "" ~} -INSTALL_METALLB=true -%{ else } -%{ if ip_pool != "" ~} -INSTALL_METALLB=true -%{ else } -INSTALL_METALLB=false -%{ endif ~} -%{ endif ~} -[ $${INSTALL_METALLB} == true ] && install_metallb || true -%{ endif ~} -%{ if deploy_demo != "" ~} -deploy_demo -%{ endif ~} diff --git a/modules/k3s_cluster/README.md b/modules/kube_cluster/README.md similarity index 62% rename from modules/k3s_cluster/README.md rename to modules/kube_cluster/README.md index b40cf58..444cb79 100644 --- a/modules/k3s_cluster/README.md +++ b/modules/kube_cluster/README.md @@ -1,6 +1,6 @@ -# K3S Cluster In-line Module +# K3s/RKE2 Cluster In-line Module -This in-line module deploys the K3S cluster. +This in-line module deploys the K3s/RKE2 cluster. ## Notes @@ -10,10 +10,6 @@ This in-line module deploys the K3S cluster. See [this](https://discuss.hashicorp.com/t/invalid-value-for-vars-parameter-vars-map-does-not-contain-key-issue/12074/4) and [this](https://github.com/hashicorp/terraform/issues/23384) for more information. -* The loopback interface for API LB cannot be up until K3s is fully installed in the extra control plane nodes - - Otherwise they will try to join themselves... that's why there is a curl to the K3s API that waits for the first master to be up before trying to install K3s and also why the bird configuration happens after K3s is up and running in the other nodes. - * ServiceLB disabled `--disable servicelb` is required for metallb to work @@ -30,8 +26,8 @@ This in-line module deploys the K3S cluster. | Name | Version | |------|---------| -| [equinix](#provider\_equinix) | >= 1.14.2 | -| [random](#provider\_random) | >= 3.5.1 | +| [equinix](#provider\_equinix) | 2.5.0 | +| [random](#provider\_random) | 3.6.3 | ### Modules @@ -50,33 +46,43 @@ No modules. | [equinix_metal_device.control_plane_others](https://registry.terraform.io/providers/equinix/equinix/latest/docs/resources/metal_device) | resource | | [equinix_metal_device.nodes](https://registry.terraform.io/providers/equinix/equinix/latest/docs/resources/metal_device) | resource | | [equinix_metal_reserved_ip_block.api_vip_addr](https://registry.terraform.io/providers/equinix/equinix/latest/docs/resources/metal_reserved_ip_block) | resource | +| [equinix_metal_reserved_ip_block.ingress_addr](https://registry.terraform.io/providers/equinix/equinix/latest/docs/resources/metal_reserved_ip_block) | resource | | [equinix_metal_reserved_ip_block.ip_pool](https://registry.terraform.io/providers/equinix/equinix/latest/docs/resources/metal_reserved_ip_block) | resource | -| [random_string.random_k3s_token](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | +| [random_string.random_password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | +| [random_string.random_token](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | ### Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [metal\_metro](#input\_metal\_metro) | Equinix Metal Metro | `string` | n/a | yes | -| [metal\_project\_id](#input\_metal\_project\_id) | Equinix Metal Project ID | `string` | n/a | yes | -| [cluster\_name](#input\_cluster\_name) | Cluster name | `string` | `"K3s cluster"` | no | +| [cluster\_name](#input\_cluster\_name) | Cluster name | `string` | `"Cluster"` | no | | [control\_plane\_hostnames](#input\_control\_plane\_hostnames) | Control plane hostname prefix | `string` | `"cp"` | no | -| [custom\_k3s\_token](#input\_custom\_k3s\_token) | K3s token used for nodes to join the cluster (autogenerated otherwise) | `string` | `null` | no | +| [custom\_rancher\_password](#input\_custom\_rancher\_password) | Rancher initial password (autogenerated if not provided) | `string` | `null` | no | +| [custom\_token](#input\_custom\_token) | Token used for nodes to join the cluster (autogenerated otherwise) | `string` | `null` | no | | [deploy\_demo](#input\_deploy\_demo) | Deploys a simple demo using a global IP as ingress and a hello-kubernetes pods | `bool` | `false` | no | | [global\_ip\_cidr](#input\_global\_ip\_cidr) | Global Anycast IP that will be mapped on all metros via BGP | `string` | `null` | no | -| [ip\_pool\_count](#input\_ip\_pool\_count) | Number of public IPv4 per metro to be used as LoadBalancers with MetalLB | `number` | `0` | no | -| [k3s\_ha](#input\_k3s\_ha) | K3s HA (aka 3 control plane nodes) | `bool` | `false` | no | -| [k3s\_version](#input\_k3s\_version) | K3s version to be installed. Empty for latest | `string` | `""` | no | +| [ha](#input\_ha) | HA (aka 3 control plane nodes) | `bool` | `false` | no | +| [ip\_pool\_count](#input\_ip\_pool\_count) | Number of public IPv4 per metro to be used as LoadBalancers with MetalLB (it needs to be power of 2 between 0 and 256 as required by Equinix Metal) | `number` | `0` | no | +| [kube\_version](#input\_kube\_version) | K3s/RKE2 version to be installed. Empty for latest K3s | `string` | `""` | no | +| [metal\_metro](#input\_metal\_metro) | Equinix Metal Metro | `string` | n/a | yes | +| [metal\_project\_id](#input\_metal\_project\_id) | Equinix Metal Project ID | `string` | n/a | yes | | [metallb\_version](#input\_metallb\_version) | MetalLB version to be installed. Empty for latest | `string` | `""` | no | -| [node\_count](#input\_node\_count) | Number of K3s nodes | `number` | `"0"` | no | +| [node\_count](#input\_node\_count) | Number of nodes | `number` | `"0"` | no | | [node\_hostnames](#input\_node\_hostnames) | Node hostname prefix | `string` | `"node"` | no | | [os](#input\_os) | Operating system | `string` | `"debian_11"` | no | -| [plan\_control\_plane](#input\_plan\_control\_plane) | K3s control plane type/size | `string` | `"c3.small.x86"` | no | -| [plan\_node](#input\_plan\_node) | K3s node type/size | `string` | `"c3.small.x86"` | no | +| [plan\_control\_plane](#input\_plan\_control\_plane) | Control plane type/size | `string` | `"c3.small.x86"` | no | +| [plan\_node](#input\_plan\_node) | Node type/size | `string` | `"c3.small.x86"` | no | +| [rancher\_flavor](#input\_rancher\_flavor) | Rancher flavor to be installed (prime, latest, stable or alpha). Empty to not install it | `string` | `""` | no | +| [rancher\_version](#input\_rancher\_version) | Rancher version to be installed (vX.Y.Z). Empty for latest | `string` | `""` | no | ### Outputs | Name | Description | |------|-------------| -| [k3s\_api\_ip](#output\_k3s\_api\_ip) | K3s API IPs | +| [ingress\_ip](#output\_ingress\_ip) | Ingress IP | +| [ip\_pool\_cidr](#output\_ip\_pool\_cidr) | IP Pool for LoadBalancer SVCs | +| [kube\_api\_ip](#output\_kube\_api\_ip) | K8s API IPs | +| [nodes\_details](#output\_nodes\_details) | Nodes external and internal IPs | +| [rancher\_address](#output\_rancher\_address) | Rancher URL | +| [rancher\_password](#output\_rancher\_password) | Rancher initial password | diff --git a/modules/k3s_cluster/main.tf b/modules/kube_cluster/main.tf similarity index 68% rename from modules/k3s_cluster/main.tf rename to modules/kube_cluster/main.tf index b9906fa..15119b8 100644 --- a/modules/k3s_cluster/main.tf +++ b/modules/kube_cluster/main.tf @@ -1,10 +1,17 @@ locals { - k3s_token = coalesce(var.custom_k3s_token, random_string.random_k3s_token.result) - api_vip = var.k3s_ha ? equinix_metal_reserved_ip_block.api_vip_addr[0].address : equinix_metal_device.all_in_one[0].network[0].address + token = coalesce(var.custom_token, random_string.random_token.result) + rancher_pass = var.custom_rancher_password != null ? coalesce(var.custom_rancher_password, random_string.random_password.result) : null + api_vip = var.ha ? equinix_metal_reserved_ip_block.api_vip_addr[0].address : equinix_metal_device.all_in_one[0].network[0].address + ingress_ip = var.ip_pool_count > 0 ? equinix_metal_reserved_ip_block.ingress_addr[0].address : "" ip_pool_cidr = var.ip_pool_count > 0 ? equinix_metal_reserved_ip_block.ip_pool[0].cidr_notation : "" } -resource "random_string" "random_k3s_token" { +resource "random_string" "random_token" { + length = 16 + special = false +} + +resource "random_string" "random_password" { length = 16 special = false } @@ -20,32 +27,45 @@ resource "equinix_metal_device" "control_plane_master" { operating_system = var.os billing_cycle = "hourly" project_id = var.metal_project_id - count = var.k3s_ha ? 1 : 0 + count = var.ha ? 1 : 0 description = var.cluster_name user_data = templatefile("${path.module}/templates/user-data.tftpl", { - k3s_token = local.k3s_token, + token = local.token, API_IP = local.api_vip, + ingress_ip = local.ingress_ip, global_ip_cidr = var.global_ip_cidr, ip_pool = local.ip_pool_cidr, - k3s_version = var.k3s_version, + kube_version = var.kube_version, metallb_version = var.metallb_version, deploy_demo = var.deploy_demo, + rancher_flavor = var.rancher_flavor, + rancher_version = var.rancher_version, + rancher_pass = local.rancher_pass, node_type = "control-plane-master" }) } resource "equinix_metal_bgp_session" "control_plane_master" { device_id = equinix_metal_device.control_plane_master[0].id address_family = "ipv4" - count = var.k3s_ha ? 1 : 0 + count = var.ha ? 1 : 0 } resource "equinix_metal_reserved_ip_block" "api_vip_addr" { - count = var.k3s_ha ? 1 : 0 + count = var.ha ? 1 : 0 + project_id = var.metal_project_id + metro = var.metal_metro + type = "public_ipv4" + quantity = 1 + description = "Kubernetes API IP for the ${var.cluster_name} cluster" +} + +resource "equinix_metal_reserved_ip_block" "ingress_addr" { + count = var.ip_pool_count > 0 ? 1 : 0 project_id = var.metal_project_id metro = var.metal_metro type = "public_ipv4" quantity = 1 - description = "K3s API IP" + description = "Ingress IP for the ${var.cluster_name} cluster" } resource "equinix_metal_device" "control_plane_others" { @@ -55,16 +75,20 @@ resource "equinix_metal_device" "control_plane_others" { operating_system = var.os billing_cycle = "hourly" project_id = var.metal_project_id - count = var.k3s_ha ? 2 : 0 + count = var.ha ? 2 : 0 description = var.cluster_name depends_on = [equinix_metal_device.control_plane_master] user_data = templatefile("${path.module}/templates/user-data.tftpl", { - k3s_token = local.k3s_token, + token = local.token, API_IP = local.api_vip, + ingress_ip = local.ingress_ip, global_ip_cidr = "", ip_pool = "", - k3s_version = var.k3s_version, + kube_version = var.kube_version, metallb_version = var.metallb_version, + rancher_flavor = var.rancher_flavor, + rancher_version = var.rancher_version, + rancher_pass = local.rancher_pass, deploy_demo = false, node_type = "control-plane" }) } @@ -72,13 +96,13 @@ resource "equinix_metal_device" "control_plane_others" { resource "equinix_metal_bgp_session" "control_plane_second" { device_id = equinix_metal_device.control_plane_others[0].id address_family = "ipv4" - count = var.k3s_ha ? 1 : 0 + count = var.ha ? 1 : 0 } resource "equinix_metal_bgp_session" "control_plane_third" { device_id = equinix_metal_device.control_plane_others[1].id address_family = "ipv4" - count = var.k3s_ha ? 1 : 0 + count = var.ha ? 1 : 0 } ################################################################################ @@ -91,7 +115,7 @@ resource "equinix_metal_reserved_ip_block" "ip_pool" { quantity = var.ip_pool_count metro = var.metal_metro count = var.ip_pool_count > 0 ? 1 : 0 - description = "IP Pool to be used for LoadBalancers via MetalLB" + description = "IP Pool to be used for LoadBalancers via MetalLB on the ${var.cluster_name} cluster" } ################################################################################ @@ -109,12 +133,16 @@ resource "equinix_metal_device" "nodes" { description = var.cluster_name depends_on = [equinix_metal_device.control_plane_master] user_data = templatefile("${path.module}/templates/user-data.tftpl", { - k3s_token = local.k3s_token, + token = local.token, API_IP = local.api_vip, + ingress_ip = local.ingress_ip, global_ip_cidr = "", ip_pool = "", - k3s_version = var.k3s_version, + kube_version = var.kube_version, metallb_version = var.metallb_version, + rancher_flavor = var.rancher_flavor, + rancher_version = var.rancher_version, + rancher_pass = local.rancher_pass, deploy_demo = false, node_type = "node" }) } @@ -130,16 +158,20 @@ resource "equinix_metal_device" "all_in_one" { operating_system = var.os billing_cycle = "hourly" project_id = var.metal_project_id - count = var.k3s_ha ? 0 : 1 + count = var.ha ? 0 : 1 description = var.cluster_name user_data = templatefile("${path.module}/templates/user-data.tftpl", { - k3s_token = local.k3s_token, + token = local.token, global_ip_cidr = var.global_ip_cidr, ip_pool = local.ip_pool_cidr, API_IP = "", - k3s_version = var.k3s_version, + ingress_ip = local.ingress_ip, + kube_version = var.kube_version, metallb_version = var.metallb_version, deploy_demo = var.deploy_demo, + rancher_flavor = var.rancher_flavor, + rancher_version = var.rancher_version, + rancher_pass = local.rancher_pass, node_type = "all-in-one" }) } @@ -147,5 +179,5 @@ resource "equinix_metal_device" "all_in_one" { resource "equinix_metal_bgp_session" "all_in_one" { device_id = equinix_metal_device.all_in_one[0].id address_family = "ipv4" - count = var.k3s_ha ? 0 : 1 + count = var.ha ? 0 : 1 } diff --git a/modules/kube_cluster/outputs.tf b/modules/kube_cluster/outputs.tf new file mode 100644 index 0000000..11050d5 --- /dev/null +++ b/modules/kube_cluster/outputs.tf @@ -0,0 +1,34 @@ +output "kube_api_ip" { + value = local.api_vip + description = "K8s API IPs" +} + +output "rancher_address" { + value = var.rancher_flavor != "" ? "https://rancher.${local.ingress_ip}.sslip.io" : null + description = "Rancher URL" +} + +output "rancher_password" { + value = var.rancher_flavor != "" ? local.rancher_pass : null + description = "Rancher initial password" +} + +output "ingress_ip" { + value = var.ip_pool_count > 0 ? local.ingress_ip : null + description = "Ingress IP" +} + +output "ip_pool_cidr" { + value = var.ip_pool_count > 0 ? local.ip_pool_cidr : null + description = "IP Pool for LoadBalancer SVCs" +} + +output "nodes_details" { + value = { + for node in flatten([equinix_metal_device.control_plane_master, equinix_metal_device.control_plane_others, equinix_metal_device.nodes, equinix_metal_device.all_in_one]) : node.hostname => { + node_private_ipv4 = node.access_private_ipv4 + node_public_ipv4 = node.access_public_ipv4 + } + } + description = "Nodes external and internal IPs" +} diff --git a/modules/kube_cluster/templates/user-data.tftpl b/modules/kube_cluster/templates/user-data.tftpl new file mode 100644 index 0000000..95c0fb3 --- /dev/null +++ b/modules/kube_cluster/templates/user-data.tftpl @@ -0,0 +1,707 @@ +#!/usr/bin/env bash +set -euo pipefail + +die(){ + echo $${1} >&2 + exit $${2} +} + +prechecks(){ + # Set OS + source /etc/os-release + case $${ID} in + "debian") + export PKGMANAGER="apt" + ;; + "sles") + export PKGMANAGER="zypper" + ;; + "sle-micro") + export PKGMANAGER="transactional-update" + ;; + *) + die "Unsupported OS $${ID}" 1 + ;; + esac + # Set ARCH + ARCH=$(uname -m) + case $${ARCH} in + "amd64") + export ARCH=amd64 + export SUFFIX= + ;; + "x86_64") + export ARCH=amd64 + export SUFFIX= + ;; + "arm64") + export ARCH=arm64 + export SUFFIX=-$${ARCH} + ;; + "s390x") + export ARCH=s390x + export SUFFIX=-$${ARCH} + ;; + "aarch64") + export ARCH=arm64 + export SUFFIX=-$${ARCH} + ;; + "arm*") + export ARCH=arm + export SUFFIX=-$${ARCH}hf + ;; + *) + die "Unsupported architecture $${ARCH}" 1 + ;; + esac +} + +prereqs(){ + # Required packages + case $${PKGMANAGER} in + "apt") + apt update + apt install -y jq curl + ;; + "zypper") + zypper refresh + zypper install -y jq curl + ;; + esac +} + +wait_for_kube_api(){ + # Wait for the node to be available, meaning the K8s API is available + while ! kubectl wait --for condition=ready node $(cat /etc/hostname | tr '[:upper:]' '[:lower:]') --timeout=60s; do sleep 2 ; done +} + +install_eco(){ + # Wait for K3s to be up. It should be up already but just in case. + wait_for_kube_api + + # Download helm as required to install endpoint-copier-operator + command -v helm || curl -fsSL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 |bash + + # Add the SUSE Edge charts and deploy ECO + helm repo add suse-edge https://suse-edge.github.io/charts + helm repo update + helm install --create-namespace -n endpoint-copier-operator endpoint-copier-operator suse-edge/endpoint-copier-operator + + # Configure the MetalLB IP Address pool for the VIP + cat <<-EOF | kubectl apply -f - + apiVersion: metallb.io/v1beta1 + kind: IPAddressPool + metadata: + name: kubernetes-vip-ip-pool + namespace: metallb-system + spec: + addresses: + - ${API_IP}/32 + serviceAllocation: + priority: 100 + namespaces: + - default + EOF + + # Create the kubernetes-vip service that will be updated by e-c-o with the control plane hosts + if [[ $${KUBETYPE} == "k3s" ]]; then + cat <<-EOF | kubectl apply -f - + apiVersion: v1 + kind: Service + metadata: + name: kubernetes-vip + namespace: default + spec: + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: k8s-api + port: 6443 + protocol: TCP + targetPort: 6443 + type: LoadBalancer + EOF + fi + if [[ $${KUBETYPE} == "rke2" ]]; then + cat <<-EOF | kubectl apply -f - + apiVersion: v1 + kind: Service + metadata: + name: kubernetes-vip + namespace: default + spec: + internalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: k8s-api + port: 6443 + protocol: TCP + targetPort: 6443 + - name: rke2-api + port: 9345 + protocol: TCP + targetPort: 9345 + type: LoadBalancer + EOF + fi +} + +install_metallb(){ +%{ if metallb_version != "" ~} + export METALLB_VERSION=${metallb_version} +%{ else ~} + export METALLB_VERSION=$(curl --silent "https://api.github.com/repos/metallb/metallb/releases/latest" | jq -r .tag_name) +%{ endif ~} + + # Wait for K3s to be up. It should be up already but just in case. + wait_for_kube_api + + # Apply the MetalLB manifest + kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/$${METALLB_VERSION}/config/manifests/metallb-native.yaml + + # Wait for MetalLB to be up + while ! kubectl wait --for condition=ready -n metallb-system $(kubectl get pods -n metallb-system -l component=controller -o name) --timeout=10s; do sleep 2 ; done + + # In order to configure MetalLB, the metadata information is required. + # BGP info can take a few seconds to be populated, retry if that's the case + INTERNAL_IP="null" + while [ $${INTERNAL_IP} == "null" ]; do + echo "BGP data still not available..." + sleep 5 + METADATA=$(curl -s https://metadata.platformequinix.com/metadata) + INTERNAL_IP=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].customer_ip') + done + PEER_IP_1=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].peer_ips[0]') + PEER_IP_2=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].peer_ips[1]') + ASN=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].customer_as') + ASN_AS=$(echo $${METADATA} | jq -r '.bgp_neighbors[0].peer_as') + +%{ if global_ip_cidr != "" ~} + # Configure the IPAddressPool for the Global IP if present + cat <<- EOF | kubectl apply -f - + apiVersion: metallb.io/v1beta1 + kind: IPAddressPool + metadata: + name: anycast-ip + namespace: metallb-system + spec: + addresses: + - ${global_ip_cidr} + autoAssign: true + avoidBuggyIPs: false + serviceAllocation: + namespaces: + - ingress-nginx-global + priority: 100 + serviceSelectors: + - matchExpressions: + - key: ingress-type + operator: In + values: + - ingress-nginx-global + EOF +%{ endif ~} + +%{ if ingress_ip != "" ~} + if [ "$${KUBETYPE}" == "k3s" ]; then + # Configure an IPAddressPool for Ingress only + cat <<- EOF | kubectl apply -f - + apiVersion: metallb.io/v1beta1 + kind: IPAddressPool + metadata: + name: ingress + namespace: metallb-system + spec: + addresses: + - ${ingress_ip}/32 + serviceAllocation: + priority: 100 + serviceSelectors: + - matchExpressions: + - {key: app.kubernetes.io/name, operator: In, values: [traefik]} + EOF + fi + if [ "$${KUBETYPE}" == "rke2" ]; then + # Configure an IPAddressPool for Ingress only + cat <<- EOF | kubectl apply -f - + apiVersion: metallb.io/v1beta1 + kind: IPAddressPool + metadata: + name: ingress + namespace: metallb-system + spec: + addresses: + - ${ingress_ip}/32 + serviceAllocation: + priority: 100 + serviceSelectors: + - matchExpressions: + - {key: app.kubernetes.io/name, operator: In, values: [rke2-ingress-nginx]} + EOF + fi +%{ endif ~} + +%{ if ip_pool != "" ~} + # Configure the IPAddressPool for the IP pool if present + cat <<- EOF | kubectl apply -f - + apiVersion: metallb.io/v1beta1 + kind: IPAddressPool + metadata: + name: ippool + namespace: metallb-system + spec: + addresses: + - ${ip_pool} + autoAssign: false + EOF +%{ endif ~} + + # Configure the BGPPeer for each peer IP + cat <<- EOF | kubectl apply -f - + apiVersion: metallb.io/v1beta2 + kind: BGPPeer + metadata: + name: equinix-metal-peer-1 + namespace: metallb-system + spec: + peerASN: $${ASN_AS} + myASN: $${ASN} + peerAddress: $${PEER_IP_1} + sourceAddress: $${INTERNAL_IP} + EOF + + cat <<- EOF | kubectl apply -f - + apiVersion: metallb.io/v1beta2 + kind: BGPPeer + metadata: + name: equinix-metal-peer-1 + namespace: metallb-system + spec: + peerASN: $${ASN_AS} + myASN: $${ASN} + peerAddress: $${PEER_IP_2} + sourceAddress: $${INTERNAL_IP} + EOF + + # Enable the BGPAdvertisement, only to be executed in the control-plane nodes + cat <<- EOF | kubectl apply -f - + apiVersion: metallb.io/v1beta1 + kind: BGPAdvertisement + metadata: + name: bgp-peers + namespace: metallb-system + spec: + nodeSelectors: + - matchLabels: + node-role.kubernetes.io/control-plane: "true" + EOF +} + +install_k3s(){ + # Download the K3s installer script + curl -L --output k3s_installer.sh https://get.k3s.io && install -m755 k3s_installer.sh /usr/local/bin/ + +%{ if node_type == "control-plane" ~} + # If the node to be installed is the second or third control plane or extra nodes, wait for the API to be up + # Wait for the first control plane node to be up + while ! curl -m 10 -s -k -o /dev/null https://${API_IP}:6443 ; do echo "API still not reachable"; sleep 2 ; done +%{ endif ~} +%{ if node_type == "node" ~} + # Wait for the first control plane node to be up + while ! curl -m 10 -s -k -o /dev/null https://${API_IP}:6443 ; do echo "API still not reachable"; sleep 2 ; done +%{ endif ~} + + export INSTALL_K3S_SKIP_ENABLE=false + export INSTALL_K3S_SKIP_START=false + export K3S_TOKEN="${token}" + export NODE_IP=$(curl -s https://metadata.platformequinix.com/metadata | jq -r '.network.addresses[] | select(.public == false and .address_family == 4) |.address') + export NODE_EXTERNAL_IP=$(curl -s https://metadata.platformequinix.com/metadata | jq -r '.network.addresses[] | select(.public == true and .address_family == 4) |.address') +%{ if node_type == "all-in-one" ~} +%{ if global_ip_cidr != "" ~} + export INSTALL_K3S_EXEC="server --write-kubeconfig-mode=644 --disable=servicelb --node-ip $${NODE_IP} --node-external-ip $${NODE_EXTERNAL_IP}" +%{ else ~} +%{ if ip_pool != "" ~} + export INSTALL_K3S_EXEC="server --write-kubeconfig-mode=644 --disable=servicelb --node-ip $${NODE_IP} --node-external-ip $${NODE_EXTERNAL_IP}" +%{ else ~} + export INSTALL_K3S_EXEC="server --write-kubeconfig-mode=644 --node-ip $${NODE_IP} --node-external-ip $${NODE_EXTERNAL_IP}" +%{ endif ~} +%{ endif ~} +%{ endif ~} +%{ if node_type == "control-plane-master" ~} + export INSTALL_K3S_EXEC="server --cluster-init --write-kubeconfig-mode=644 --tls-san=${API_IP} --tls-san=${API_IP}.sslip.io --disable=servicelb --node-ip $${NODE_IP} --node-external-ip $${NODE_EXTERNAL_IP}" +%{ endif ~} +%{ if node_type == "control-plane" ~} + export INSTALL_K3S_EXEC="server --server https://${API_IP}:6443 --write-kubeconfig-mode=644 --node-ip $${NODE_IP} --node-external-ip $${NODE_EXTERNAL_IP}" +%{ endif ~} +%{ if node_type == "node" ~} + export INSTALL_K3S_EXEC="agent --server https://${API_IP}:6443 --node-ip $${NODE_IP} --node-external-ip $${NODE_EXTERNAL_IP}" +%{ endif ~} +%{ if kube_version != "" ~} + export INSTALL_K3S_VERSION="${kube_version}" +%{ endif ~} + /usr/local/bin/k3s_installer.sh +} + +install_rke2(){ + # Download the RKE2 installer script + curl -L --output rke2_installer.sh https://get.rke2.io && install -m755 rke2_installer.sh /usr/local/bin/ + + # RKE2 configuration is set via config.yaml file + mkdir -p /etc/rancher/rke2/ + +%{ if node_type == "control-plane" ~} + # If the node to be installed is the second or third control plane or extra nodes, wait for the API to be up + # Wait for the first control plane node to be up + while ! curl -m 10 -s -k -o /dev/null https://${API_IP}:6443 ; do echo "API still not reachable"; sleep 2 ; done +%{ endif ~} +%{ if node_type == "node" ~} + # Wait for the first control plane node to be up + while ! curl -m 10 -s -k -o /dev/null https://${API_IP}:6443 ; do echo "API still not reachable"; sleep 2 ; done +%{ endif ~} + + export RKE2_TOKEN="${token}" + export NODE_IP=$(curl -s https://metadata.platformequinix.com/metadata | jq -r '.network.addresses[] | select(.public == false and .address_family == 4) |.address') + export NODE_EXTERNAL_IP=$(curl -s https://metadata.platformequinix.com/metadata | jq -r '.network.addresses[] | select(.public == true and .address_family == 4) |.address') +%{ if node_type == "all-in-one" ~} + export INSTALL_RKE2_TYPE="server" + cat <<- EOF >> /etc/rancher/rke2/config.yaml + token: $${RKE2_TOKEN} + write-kubeconfig-mode: "0644" + node-ip: $${NODE_IP} + node-external-ip: $${NODE_EXTERNAL_IP} + EOF +%{ endif ~} +%{ if node_type == "control-plane-master" ~} + export INSTALL_RKE2_TYPE="server" + cat <<- EOF >> /etc/rancher/rke2/config.yaml + token: $${RKE2_TOKEN} + write-kubeconfig-mode: "0644" + node-ip: $${NODE_IP} + node-external-ip: $${NODE_EXTERNAL_IP} + tls-san: + - "${API_IP}" + - "${API_IP}.sslip.io" + EOF +%{ endif ~} +%{ if node_type == "control-plane" ~} + export INSTALL_RKE2_TYPE="server" + cat <<- EOF >> /etc/rancher/rke2/config.yaml + server: https://${API_IP}:9345 + token: $${RKE2_TOKEN} + write-kubeconfig-mode: "0644" + node-ip: $${NODE_IP} + node-external-ip: $${NODE_EXTERNAL_IP} + EOF +%{ endif ~} +%{ if node_type == "node" ~} + export INSTALL_RKE2_TYPE="agent" + cat <<- EOF >> /etc/rancher/rke2/config.yaml + server: https://${API_IP}:9345 + token: $${RKE2_TOKEN} + write-kubeconfig-mode: "0644" + node-ip: $${NODE_IP} + node-external-ip: $${NODE_EXTERNAL_IP} + EOF +%{ endif ~} +%{ if ingress_ip != "" ~} + mkdir -p /var/lib/rancher/rke2/server/manifests/ + cat <<- EOF >> /var/lib/rancher/rke2/server/manifests/rke2-ingress-config.yaml + apiVersion: helm.cattle.io/v1 + kind: HelmChartConfig + metadata: + name: rke2-ingress-nginx + namespace: kube-system + spec: + valuesContent: |- + controller: + config: + use-forwarded-headers: "true" + enable-real-ip: "true" + publishService: + enabled: true + service: + enabled: true + type: LoadBalancer + externalTrafficPolicy: Local + EOF +%{ endif ~} +%{ if kube_version != "" ~} + export INSTALL_RKE2_VERSION="${kube_version}" +%{ endif ~} + /usr/local/bin/rke2_installer.sh + systemctl enable --now rke2-$${INSTALL_RKE2_TYPE} +} + +deploy_demo(){ + # Check if the demo is already deployed + if kubectl get deployment -n hello-kubernetes hello-kubernetes -o name > /dev/null 2>&1; then exit 0; fi + + if [ "$${KUBETYPE}" == "rke2" ]; then + # Wait for the rke2-ingress-nginx-controller DS to be available if using RKE2 + while ! kubectl rollout status daemonset -n kube-system rke2-ingress-nginx-controller --timeout=60s; do sleep 2 ; done + fi + # I cannot make split work in Terraform templates + IP=$(echo ${global_ip_cidr} | cut -d/ -f1) + cat <<- EOF | kubectl apply -f - + --- + apiVersion: v1 + kind: Namespace + metadata: + name: hello-kubernetes + --- + apiVersion: v1 + kind: ServiceAccount + metadata: + name: hello-kubernetes + namespace: hello-kubernetes + labels: + app.kubernetes.io/name: hello-kubernetes + --- + apiVersion: v1 + kind: Service + metadata: + name: hello-kubernetes + namespace: hello-kubernetes + labels: + app.kubernetes.io/name: hello-kubernetes + spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: hello-kubernetes + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: hello-kubernetes + namespace: hello-kubernetes + labels: + app.kubernetes.io/name: hello-kubernetes + spec: + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: hello-kubernetes + template: + metadata: + labels: + app.kubernetes.io/name: hello-kubernetes + spec: + serviceAccountName: hello-kubernetes + containers: + - name: hello-kubernetes + image: "paulbouwer/hello-kubernetes:1.10" + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + env: + - name: HANDLER_PATH_PREFIX + value: "" + - name: RENDER_PATH_PREFIX + value: "" + - name: KUBERNETES_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: KUBERNETES_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: KUBERNETES_NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: CONTAINER_IMAGE + value: "paulbouwer/hello-kubernetes:1.10" + --- + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: hello-kubernetes-ingress + namespace: hello-kubernetes + spec: + ingressClassName: ingress-nginx-global + rules: + - host: hellok3s.$${IP}.sslip.io + http: + paths: + - path: "/" + pathType: Prefix + backend: + service: + name: hello-kubernetes + port: + name: http + EOF +} + +install_rancher(){ + # Wait for Kube API to be up. It should be up already but just in case. + wait_for_kube_api + + # Download helm as required to install Rancher + command -v helm || curl -fsSL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 |bash + + # Get latest Cert-manager version + CMVERSION=$(curl -s "https://api.github.com/repos/cert-manager/cert-manager/releases/latest" | jq -r '.tag_name') + + RANCHERFLAVOR=${rancher_flavor} + # https://ranchermanager.docs.rancher.com/pages-for-subheaders/install-upgrade-on-a-kubernetes-cluster + case $${RANCHERFLAVOR} in + "latest" | "stable" | "alpha") + helm repo add rancher https://releases.rancher.com/server-charts/$${RANCHERFLAVOR} + ;; + "prime") + helm repo add rancher https://charts.rancher.com/server-charts/prime + ;; + *) + echo "Rancher flavor not detected, using latest" + helm repo add rancher https://releases.rancher.com/server-charts/latest + ;; + esac + + helm repo add jetstack https://charts.jetstack.io + helm repo update + + # Install the cert-manager Helm chart + helm install cert-manager jetstack/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --set crds.enabled=true \ + --version $${CMVERSION} + + IP="" + # https://github.com/rancher/rke2/issues/3958 + if [ "$${KUBETYPE}" == "rke2" ]; then + # Wait for the rke2-ingress-nginx-controller DS to be available if using RKE2 + while ! kubectl rollout status daemonset -n kube-system rke2-ingress-nginx-controller --timeout=60s; do sleep 2 ; done + IP=$(kubectl get svc -n kube-system rke2-ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + fi + + # Get the IP of the ingress object if provided + if [ "$${KUBETYPE}" == "k3s" ]; then + IP=$(kubectl get svc -n kube-system traefik -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + fi + + if [[ $${IP} == "" ]]; then + # Just use internal IPs + IP=$(hostname -I | awk '{print $1}') + fi + + # Install rancher using sslip.io as hostname and with just a single replica + helm install rancher rancher/rancher \ + --namespace cattle-system \ + --create-namespace \ + --set hostname=rancher.$${IP}.sslip.io \ + --set bootstrapPassword="${rancher_pass}" \ + --set replicas=1 \ + --set global.cattle.psp.enabled=false %{ if rancher_version != "" ~}--version "${rancher_version}"%{ endif ~} + + while ! kubectl wait --for condition=ready -n cattle-system $(kubectl get pods -n cattle-system -l app=rancher -o name) --timeout=10s; do sleep 2 ; done +} + +install_global_ingress(){ + command -v helm || curl -fsSL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 |bash + + cat <<- EOF > ingress-nginx-global.yaml + controller: + ingressClassResource: + name: ingress-nginx-global + controllerValue: k8s.io/ingress-nginx-global + service: + labels: + ingress-type: ingress-nginx-global + admissionWebhooks: + enabled: false + EOF + + helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx + helm repo update + helm install -f ingress-nginx-global.yaml ingress-nginx-global --namespace ingress-nginx-global --create-namespace ingress-nginx/ingress-nginx +} + +prechecks +prereqs + +if [[ "${kube_version}" =~ .*"k3s".* ]] || [[ "${kube_version}" == "" ]]; then + export KUBETYPE="k3s" + export KUBECONFIG=/etc/rancher/k3s/k3s.yaml + echo "export KUBECONFIG=/etc/rancher/k3s/k3s.yaml" >> /etc/profile.d/k3s.sh + install_k3s + mkdir -p /root/.kube/ + ln -s /etc/rancher/k3s/k3s.yaml /root/.kube/config +elif [[ "${kube_version}" =~ .*"rke2".* ]]; then + export KUBETYPE="rke2" + ln -s /var/lib/rancher/rke2/bin/kubectl /usr/local/bin/kubectl + export KUBECONFIG=/etc/rancher/rke2/rke2.yaml + echo "export KUBECONFIG=/etc/rancher/rke2/rke2.yaml" >> /etc/profile.d/rke2.sh + install_rke2 + mkdir -p /root/.kube/ + ln -s /etc/rancher/rke2/rke2.yaml /root/.kube/config +else + die "Kubernetes version ${kube_version} not valid" 2 +fi + +DEPLOY_DEMO=false +INSTALL_METALLB=false +INSTALL_RANCHER=false +INSTALL_GLOBAL_INGRESS=false + +%{ if node_type == "control-plane-master" ~} +INSTALL_METALLB=true +%{ if global_ip_cidr != "" ~} +INSTALL_GLOBAL_INGRESS=true +%{ endif ~} +%{ if deploy_demo != "false" ~} +DEPLOY_DEMO=true +%{ endif ~} +%{ if rancher_flavor != "" ~} +INSTALL_RANCHER=true +%{ endif ~} +%{ endif ~} + +%{ if node_type == "all-in-one" ~} +%{ if global_ip_cidr != "" ~} +INSTALL_METALLB=true +INSTALL_GLOBAL_INGRESS=true +%{ endif } +%{ if ip_pool != "" ~} +INSTALL_METALLB=true +%{ endif } +%{ if deploy_demo != "false" ~} +DEPLOY_DEMO=true +%{ endif ~} +%{ if rancher_flavor != "" ~} +INSTALL_RANCHER=true +%{ endif ~} +%{ endif ~} + +[ $${INSTALL_METALLB} == true ] && install_metallb || true + +%{ if API_IP != "" ~} +%{ if node_type == "control-plane-master" ~} +install_eco +%{ endif ~} +%{ endif ~} + +[ $${INSTALL_GLOBAL_INGRESS} == true ] && install_global_ingress || true +[ $${DEPLOY_DEMO} == true ] && deploy_demo || true +[ $${INSTALL_RANCHER} == true ] && install_rancher || true diff --git a/modules/k3s_cluster/variables.tf b/modules/kube_cluster/variables.tf similarity index 57% rename from modules/k3s_cluster/variables.tf rename to modules/kube_cluster/variables.tf index c3860a4..9cddb34 100644 --- a/modules/k3s_cluster/variables.tf +++ b/modules/kube_cluster/variables.tf @@ -17,30 +17,30 @@ variable "deploy_demo" { variable "cluster_name" { type = string description = "Cluster name" - default = "K3s cluster" + default = "Cluster" } variable "plan_control_plane" { type = string - description = "K3s control plane type/size" + description = "Control plane type/size" default = "c3.small.x86" } variable "plan_node" { type = string - description = "K3s node type/size" + description = "Node type/size" default = "c3.small.x86" } variable "node_count" { type = number - description = "Number of K3s nodes" + description = "Number of nodes" default = "0" } -variable "k3s_ha" { +variable "ha" { type = bool - description = "K3s HA (aka 3 control plane nodes)" + description = "HA (aka 3 control plane nodes)" default = false } @@ -62,16 +62,20 @@ variable "node_hostnames" { default = "node" } -variable "custom_k3s_token" { +variable "custom_token" { type = string - description = "K3s token used for nodes to join the cluster (autogenerated otherwise)" + description = "Token used for nodes to join the cluster (autogenerated otherwise)" default = null } variable "ip_pool_count" { type = number - description = "Number of public IPv4 per metro to be used as LoadBalancers with MetalLB" + description = "Number of public IPv4 per metro to be used as LoadBalancers with MetalLB (it needs to be power of 2 between 0 and 256 as required by Equinix Metal)" default = 0 + validation { + condition = contains([0, 1, 2, 4, 8, 16, 32, 64, 128, 256], var.ip_pool_count) + error_message = "The value must be a power of 2 between 0 and 256." + } } variable "global_ip_cidr" { @@ -80,9 +84,9 @@ variable "global_ip_cidr" { default = null } -variable "k3s_version" { +variable "kube_version" { type = string - description = "K3s version to be installed. Empty for latest" + description = "K3s/RKE2 version to be installed. Empty for latest K3s" default = "" } @@ -91,3 +95,21 @@ variable "metallb_version" { description = "MetalLB version to be installed. Empty for latest" default = "" } + +variable "rancher_version" { + type = string + description = "Rancher version to be installed (vX.Y.Z). Empty for latest" + default = "" +} + +variable "rancher_flavor" { + type = string + description = "Rancher flavor to be installed (prime, latest, stable or alpha). Empty to not install it" + default = "" +} + +variable "custom_rancher_password" { + type = string + description = "Rancher initial password (autogenerated if not provided)" + default = null +} diff --git a/modules/k3s_cluster/versions.tf b/modules/kube_cluster/versions.tf similarity index 100% rename from modules/k3s_cluster/versions.tf rename to modules/kube_cluster/versions.tf diff --git a/outputs.tf b/outputs.tf index 432a199..07a3c7b 100644 --- a/outputs.tf +++ b/outputs.tf @@ -8,9 +8,25 @@ output "demo_url" { description = "URL of the demo application to demonstrate a global IP shared across Metros" } -output "k3s_api" { +output "cluster_details" { value = { - for cluster in var.clusters : cluster.name => module.k3s_cluster[cluster.name].k3s_api_ip + for cluster in var.clusters : cluster.name => { + api = module.kube_cluster[cluster.name].kube_api_ip + ingress = module.kube_cluster[cluster.name].ingress_ip + ip_pool_cidr = module.kube_cluster[cluster.name].ip_pool_cidr + nodes = module.kube_cluster[cluster.name].nodes_details + } } - description = "List of Clusters => K3s APIs" + description = "List of Clusters => K8s details" +} + +output "rancher_urls" { + value = { + for cluster in var.clusters : cluster.name => { + rancher_url = cluster.rancher_flavor != "" ? module.kube_cluster[cluster.name].rancher_address : null + rancher_initial_password_base64 = cluster.rancher_flavor != "" ? base64encode(module.kube_cluster[cluster.name].rancher_password) : null + } + if module.kube_cluster[cluster.name].rancher_address != null + } + description = "List of Clusters => Rancher details" } diff --git a/rancher-clusters-imported.png b/rancher-clusters-imported.png new file mode 100644 index 0000000000000000000000000000000000000000..9aca9db8c21c6d8d3cb9a6e455daf9ce2483f2c7 GIT binary patch literal 131856 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 z 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%0D4KJp&bLt2E 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

n8Lae2H2UD2uCi@N+Y?X(*;((J$^oMU+5tLr z2>Fxe2OoyHz~XosQ8!<`-p0bDs_FUujgujeUbF?npoz?jhXrT3!Jk)4) zN06X@50rWe(eP>*>|%O!64AGg0DO`K0HeG5e3I^Yc~ZCsWz=~UOHDW5rPF7=;OKB` z*RRR}c`F+!YZsFg`FFU z!IS|awVxsv9{s^en2kt?&u2YG6ybpUET@W>+gAAIV;R7zdrm51x<=UrDE+`-*9nlgP-(HtSFUb?p_8p@Vg*O<}v ze5aGLD9iY&X3`-ykHQ0}rMc~+1D9HU_sY=>_2gk@cW-Leo=U? zt*w3O6L0X--@o`VgGXBWg)EupQ|V< ztyes+NTl05Jybrd-CQqN)6jhp64>+H(W3t<9Lnu-9;;wX`Zf4qox^T>0&qVI3&%Sa zZ`1X38G$s`$6Cx)AqVl(fVjowh@rrv`zu}~HWqXmEv|l1*l~lRox3Bv4?YG*hFC+Y zrD`I;?BD|x6;)19h~dmn1me@HbzDc0M&1dC@Y|3`PP0jqKXAF}3=}Rd?v$*_t_;Pr zd00PNp1TV%yaqnnKik;NV1VDKwI;aMBhe19!qbp=U33)=tbS`_;QkniuNbg`?b!{u z9PrjP*$4{5!hSCkza3wbu8sD&cs8)SO1^f%3l-Uf^#?;FJvAe9yj9O){yHd$!7cr& zNXYH!CK|6P;{E6VQ%}6dW|FG=Ha#jb;#>3h}!Sw9jJv@rugItWC_-9bu%UZ_%73;d_y{bz1Jn zD!=)1EGtm4nYjdtYPo7RV3cO~)xW|LH~{9uv^U@LOW$j0JD@577^qRBI-udtEA_ra zKJOmO>4_uB?owdkrd$9V9%xI%BCIB}MXEys;vT?LB?-WM0lnYVyfZKX05q>@MOE{E z_kJWLIVN}TdO0zqA}22yZS5I2qd)|x1>x?8BHKSNFEP;)>dlVzH66lxNoGXU?V7$FhJH#_)QA0rLe8iw>U#i8v3 zU-xixYrv7OK!d%C5<)JR&^a7)5LI7^6(Z*%fnm;X)52iWw3$3np-WtgQj-g4@e4O> zTM9vC#kST!JR7|L1xiBwa_lT{CVdu->-ZO)%Vs>VLoZ?o-SvoO+Mg*ZFG27vxgc?G zJ`*Jx^5rB)kh>U4V{nHa-ftZ!2y4M`%_jg-6(((|4=XwbPo zulg{p8Y1>=5Uc9TP_vDkB&A?8W-tt8+vQ3}C9(bCPn*7jg`Oe!fw>PL{Hm}GWK zAhOvr=aT1WvWX?Pyig(vm|ltsr9A|eZeN^T9A92<)3HWqzYC^N(RSkEw7)8^8ja?h z;B+1@q0h8jQ&YjHkTm$@6ds~74RWICm(iWzavdKyc)d4M?KC+M?W@+Je6d^)VGegM zC$KaesIIPCH(8(lN^7Z~^^?n`n60Vd;fqF8NLn1mnk*Qtrw!IrKCQ~04PYr;Mr4EE zJMe8wR$i&96zFL<-O`i5SXh*PIc?z*5f-jK$otOe`RJ3JlvPO~mt-c9Ub9?x@@|D% zd|i!<&AH{&>&HuUlN$%hwWl0^WJ-dq-3QwJamR>Jj0?DIgV+><#n+#4MwpZI`)Y6$_U{i78xtuQGV4SD6(GR-S8hLkP4?06L`%Z@Z) z)q=rm0k+Sp^`@Up&eroT0rhzC=iH%MOU>%Ip#w9%3r%$F87Zzh#pfhrv(b4-Ns-Zz zF$=DkY%3*D5pz?R^pWTS%RDzXRutK+&V-d74vq$rYs)+{>;0Yp8WRad8Fe=u+K*sv3I_O z+Ef61^DMI8N3j<^R4)N8S)JE1&u%NKYW1*^u2S2pbg4Fv=2sVW!LySM&MP z-uOrThgmOIc$0XIe&p{-g+oO^!^8s6|oE16Q+t50oe8LTK{m zvJ&hLV<>Wx9d#Ia2@L8Mm6A8^WcOpzm)k=hIUNt1qw+u&7R^5gW5T>pBl{CGb`0rv9{o^>Z=&8%Zb+Nh=yDTv;U`=Up_6aUS|zC+fOE=(Q+hjn;#6bci3 zxWB5lMoD764Gg-oomQSQ8b5QR;Oi9;;o{!R&U+E)4>PrItyZWSk&5m#{j1&s&!R!E>96_yz?~{s&+lTn!N|Xm zfkZSHr2L>1?~~zDlIWyCub~B9t`@9@`I@EBR&3qf*4vh17`J3n-07{w#pQ*8P&4G% zp}^0W0s{*RG6;#dOmFPylf^MUNJN+B#ZWyJX*G-kT_|+|jije%+!rEAn4al|ax3Tl z#{hiy+Kr_eYcjHq~D%Q{FGj20TH#wCs=!w{m}Af0)M>C=m?{wn%yxRfODe zT?we%PMr~YUsS|;+;rX$<+GccAJCU9%*`3P(3iZn4eO&ekx)k;CNVJ-VKBanNX}5x z`ryrP1>c)|w;6+Kr$jMhxV4NqZB!1<`iCRh4+Ln=3|&eT5=5ADpC8<5y9T--^)E-< zO)=ZYsZuJb9xBrGfA}~0Nptfbn7U18jkOdBU<2SL`c`9T{p0HiEEFJqwr;nD>9wqf zs0%MOXp170rNvNKx*%o}aEPFgH62v8Ago->Yeb}Gt4+_*r6rLd z)nv7Nm1s4wv$G&cf{F7}^;3AQMyyiGt&NTRY41Rd=V$MJA4!znMk#S|gDg5pb;LI? zT1wfm5D09O&$(gN0Zv~s270NzsaSY|O>Ehlm^r_^_Z(khS8r)>r#Q!Z(>UxTAXE=P z`h)6k)vn-U@&kEA!0#=M=Y8gSLqlV{Qg-}*FOdv@Au-$Ph*6vkAQz0RKg68zka15L z_m+AvT^iG+*&KEV4v!dCb^eb++0x!8hJf=a;g9bCo+%kLyY(`qi)oSL5$)!tvc|%c z7ciJ8v;;SQ^L-Zs`WO{8c4#J|D*HC%``2#fSIA00Oei6*yMKl~)A0IFQ=7p`?%E7l zXj0xFXBwZ_yu9K0iN2!P;Dz|eNa5+}o5kAcBBi*SvM=4q*gcJ$S@hKg$ee%E(0(E6 z*2ead`8j&NwWN-8r6^BMlu}O|Oa7$Y;u;f-U9Cq~+Fv|!vGZE(wy|_R&IZWujb75% zDA_2+SpG-F{%^mK5dRTPMaap1BTj}FGCb;+-_b~$Dyh6^e*`w*bOI>j9hQDcqmyNQ z0zUFz2GaEC6y&^P4R%miSoUdtKdY6S9H*pxOI1pO`|dRaz?T>yOz7``me@-1-9-QT z#U3TefpE0ix2KDd4-kd}DMkKvm0}G@^Zazno{xpP`rc1Q2FmbyvRHB}*o_Sbhk-DNipum+> zQ-V8v^LG&7?|h}dz>vEiKN?^Tgtf}tzf=B~3Pk;J=`n(ZyZgP4;? z`(kjZzudm*)99DM_)7?Du~iTfL00|=@<#}Zv{4h}CL|FbzrDG)S6up9rQ6F|L4kQc zv8gF$IeGB(%fC$M{tj*bxjiII2#G?Y{Zfy+e|1_Ue3)qF^k570lDMY_ImHfti1b@I zy)jZf?|Ro5&Ny0^y@ELA{CI-X)9-m9xW2pRTP4M*ftXcHK|Ga;es6Us9~AyiYgZXi z<+8N}K|lpj6hu;xE)kF}De3N%Zlt?JML;?>N_TFWO_$P0cb9Z`!#Ak+oO91n&;9Z3 zUoV^4^UkbU6KlqwoV53oOn{U~ITRB<{|xjr2NNOiWvA4%hf4EY_;g z#TCx<=TIIV{5y{sT%4G1HF@Uz*wqO;QJ#x&J1Q%UVPMwN#OLNhd|%wXn~ZU@{$BJ8 zZ0u4cm2ld%_}7`6o6q%@NREhXe3&Bk+3ab9tb4=t0&pQVb0Y6D$%r5lVOoXw$?yVG z%l%R$JY{9!ZH#ocgZRH(M?-6|5o<$l<5lm0N>k986l+vHFtyZtJ+5`FfZR^CVKBsM z=9v^iPouv{XiN-^UuvB(>NjL;YY#L&+vIjl(0@&nKL&@8`wsc1M9lSQJr)KyPv+*P zQC2=$JA&6&l(>+Y`hLKIJ;!bb4g_rI1k@#$)w!=<1_=R-S+qag>AX|dhyPs zDZNbtr{+ws-Zv#Z<~{2@@)tMW0zv^sk?u?M!tK+$FMbYH4DeVRZ@m6z1oZ`^SHSn!pdko5-$=fA#oibH$JkE6T~1mj9;@_U zEJdJ`lJsx{77J+(yZ70?HHYWLBQ1^lWd5w&%J2>|3oYA%{}q_~53zuJJ?JR4XIi`* zSx2T44$EY5G3Ymuem3reni#cLKOG(KYQ6kki{o>Ts#`>{3i#VXgFmVF?*jnON~je9 zsC>M%$$v@}{EzaELbch?gNYmm*_}XYtiQq=e*WxSNw(RL5?$5^O zZ*zf=?DBB(`cCV25!`(MzqGW3Xr2Q?<8}`wD=4U`vuoR-gc8pOv;aqRKq@APmjjec z$`StW-X%w88&T*SwieXgL`v@Hh;i?2k4nG}xW6G%#%@pad_dbgw6C2aa17?Z{nI{5 zV!^#rksLhwWcXc^@fkd?p?>Td%D3Q}*RUEqJ=(!q{DgQhCjW%o^}w7O+?PN=vQqsZ zzxLDTgj`&)*H9mb(9*^ZqgenZG`Si#olL1Z$k|WGAgaPu*XG^^BX4NIs5>4uEi+pA={5QLF5RKH!^c?N3)@wLbk7G zS8jpF&I>VHjE>v;ZuTO+_3`G4M%kUy`1Y?uMDw2PJPkt6rU1N*dfO(Yt4o#9d<Z08+QB5VUTY^U0G&;y1L6rr{V#?vN-9afutF#)DG6LF4tD+I%p3(~ z$Vh+P+y0mn*YCpZz%${9rSjaKTEq{F4&lEz+(-yjl$4TELKkQdn#;l|67ap7aHl~e zND-SZbNc)y6SOaq`U_14@&zBrNkM)e;Tjt5GbR%*zLWj6H1&$}1mFfK$SUF8ZN#zY z&ev(mP=I~}tY;-69O#gjfRo0jAn0g0YsqGRmDPCs*b$Q;6)^B%fM~xi&_Sb?5Iw+@ z?QsX6zZSiYH)OXTu*QtkTo6Ll%6vf*)SHOcG!on|#zjH+TN-flDYe-@H% z@Vz%=waRe3+Z*UV;_K&^1kQj~b_4zZxWwEQMj^Y@8cwx*(eoN*Hbc3PRM*5LN(Wys zv<~fOi!KQ0zapK*6E~4&Z$+fPR=&w>#$FZf5=|*JDkJ)3mrj%i4FgqiuaK(#~|r0B%equ3d@{|jcr@(;`g z>5^!D0SJVQ2U;n6v^^09D7-J(0Eq-AtE0bqipnMO zqhQj$Equ3+C(JFIChIXgS=WmE}If(vLFlik@svMf^&Sg|Zy zUxdw|-}4r=hAMck0qdk209wMj0PaIBXV71#8gKgPnw+Df;iEK;qVp}3f1HNEPAs*F zQhT6P$K513eiI`UV{xI1yLT(AX=!dg>4?cUb|)u_4w1%_>RHr#$wrFVaLcwsJ^ahd zVni>GdRo9{iOpm__UxI-#Rbn+~ zu;BWbsc6=nBf&E%%;XWuhkWLze&OLt=0+KgJE^VE=z5$x`Fz>+kKP6=bv}33_AhkY zi4##X944q4(TTl!qEp51jq@$1G|_C(D^az>QM2?Crtq?Dr=S zDE0F>JOfD0&644coW>62|ez!Ui4+h_pzVhl6+sYtYeeK<}L> zNEtgls2X24gY|58(DAi(v*>FKFrX{Vz!oL+tIQ3oIt%zd#+Z+uR8+Yw(>*qJ>a ztMFm+FY*hkL4JRqKFn@oB1ugsN2OS$p<#KXc!v*FMoo@ZbwAXKn*!5QuQg0z&B%n` zM2(+L_@m!58cnW-{y0)6AAv#Ovx^@yYt5JBItm(0t8wByP*Vr|1*vY6AnxI=w6Ge! zZ!rW}HWh&;A1#W*k?8DbfnfOpq`=>i8>7vC9;T$#$vGG_QnbVag4_E)*8g-{zJjbv zq}50vOZo&Dw8+md5f%3_?W5i@;Y48ZIq15!0xf((@BynQi6rlI$ZTXe*E)aCP?hw$ z?{lJy$4bMT#yM}ws{R{D@tSYuC1C5qo*TCiqPhS`iV!7dBRWgYbQJTM0145hrBXB_ z>h0|xXo?Uc6hA&|L%@#NdY!IB2SoTt9!0ww%Zb;^`iPR;qU!$!rFcS3xK+vDMX3tQ zegSJ)i;pLKa#vFvvo(mqWD9VS=_*EJ$lYXNrOrXEuw4LnzWMOEWqazh@nKAT?GCN(;S@fT6K?GrcTb zC@QwDh$f&DAU(Nx^X3!Z7p6niyck1Y761^Kjf;zGR9e4yzDQ&{&$qO(BT`qK<2lG< zC7;hpkW9P$=ONkSAzUHS*)#1SWEk(1G&*_~sZToq?tQE+{BF;%L3^&YyV0Lx`$jPgBn^ygMrHMQ@514U>N z3H|^@MEwdBVYxnrXtUV$Lhoq15$6Xe!ZT68uZJr4y)M>kzrN%9kK=2ko9OB3m*=H* zb(32mIKl}D-02U!oVR!bK zzCSfzwC)lXf!}1I3a(w=D=K(^DD-J13%)Sq+4bRSMS= zy97ok+{IzpB*|fZ5AX$#D;RIe3~0{SZ50nTQoIlyL-UO!TW6`TSxQzz>-e~rR9-i? zsurO0X?TUEJ$Yl*K^GTipEsTmJ?J7$KC@5DZ`>HbQw25Mj+k1+r``*z**Q5YCJKjg zwK^c5NIrzSjSo)Ft+((U^dD5!YwT`ih*ej+8H>B@^)oxw8MTIGxQN>^9jW9$J8&h` z@dB~Ip6b&uc~^Pp@%v-kC%UhXRf+U2POYN~qQi^Hh4JMcuksCyi_)RWnCr;HRnD=) zZ>vjF$5mBQ7Tj3g?yIop-islg<++R}wmolHo|FqIg|vSxmbsOFAD@7IAGMe?SEI@} z5;~~?GiAM?v7h!fSwG`~tPCy0xeMQ;luPivm$mts+5hN8gx4|K(4STv$Q|;S*(Nw_ zw3Yl8d|*f|_qp@XxYirBGF(8bi7L<_&`m(*mbT9{GH%b}ccCuETLN#SQj@!@M*zvA z%0brSdXl@41?xHR_CMv*I%*lU`MRwBK)F+xn>~}Y( zlOE;2d$hN4@#+q??gs0 z=jedx>R#2}0Q>TMXkVxjKiKrMKOSy=7N(YapNxiAPQGRG>Ggr!7#%=FDBF6TlJUpe zZ^Yu}8pGSJjl7vnF1x=+$sEZKm^b1yX^OEiF+UOYPd-W<0(33%m5i2jEM_Cj)cWGm zdb%+_4YA3j;tzTg1$LC`Kh_V(_Vz!Bd}>Cyx0K{;$d4dHLmP#h3eF}jtBCT71786( z?5vWdYk&3hC`8lEyfh2OkmHW+r9|wTS8O(_k7-Vp!J@(E9UwOc&QUpI=&cTOZ->A+ z(m}JtyrF9PaBE}vq&dL58vQ9xya)Oa44m4|k6GrnWz|~n+3j8d21bFhrMq=XqBGhc zpcr!0_!9AW%Il!m)P*bMG9$vF^Cup_l`NHteF|bXod9|C5@5be03^8NC3#n`-PGr; z^9JC%#=&7MC0vJ&^jMB`LU$pt2C}fociUb5^?LyNBwq{zS@gs%!M4S_54rCbz6a(W z;9-T4@*Nadjei47Fay9u!=$SIwigPg(hyrFb(jA&j~EUTLU^OjSl0}vz04w=CWXD1 z&0r}=V4iUB1K`!1MpF3D=ncmm%*6w7RHyz-CS#h?d{u24pP@3VIpYAtig#n<;ZDklNPeVemPoL<205(Ef~$HM{Dynk_3@?awP zZla#;QU}}{S@o#J`8vC%X1H)cP8N$30nQ?*OgVk8FON`3568qyMRg)7MetLq$QSJh zU5ECFiFv6hX+NSN;wARL(BygxVaBr5h1wW8hT4O-nkPnLb6ziv5ro6|}L)5QVdcFcf0El&1O0C zErz+pwGySOJBD*MoL1Gfn+Y` zN_JZm3P5F<8%!!*+MJ}?QT5NW)m0BR-`L%Za}=H0sIiyL)o@)p-1>aH;-&Q5PksSp zWazhl1EP!NBF=!FAbo52k^~(9Z34P$=bQn6vNOY54Umav1o&JTiRZ<@Y;L4%vq-vL zNjymv>t2ngst%DFq+dJ9J@v;D{rr&cS|*Zpa%^@P00iR9>6zX-VY>ut!0Z3;!$?IU zN6sRNzF-?aa?as>!ONK35`=rpYWQ3)tV0k9gDo(vTU4-j+pEn;djTc$yio_oXf1(h z^|<-fS!bGzQALm3NcBy`Ni>yjk^2@9Ie$5lhpD z4dbCn{v_gNE4<;YN?BZ_rZ-ZnclVccW8=Vtr@9?OysttKZ$Y z5Q!Cy%;fSzeS2iHa&%sw4C+r?Qisz??DJc>}T zqezM;(G$4RR{AvARn}E^(9S%drJV-EiWF^?o^-UcNMj;w`|>Cand!+J4b_~+4cAL{aHPcST;w)T;!@BPG5JD z5XE51%mCw~uWV&yW%&XPrm*;zNl;a0kWt`~>;ABuU0MaWEycXA9ZoSwVEKl8AWhro z(Hel*z1RGVD(KaojofVO0N4Ob%SMHjq59N{3C|qoo=SA2al9ma#`E%JUcqj=lEVJQ zv57a^Eg}bfu^BXF>YNET=jh;glDV%hP}V$Aj5J>tf_xMqrO`|P5kiHA_JKg=u{^$` zWB|yOwD+7}D*w|!$a+FLxa4NDGwmEW^P*O1o!)V(srgQHRX8ZoN zrkCMFeG`M84J1OBA+f3J*B@HLMvmjdsW>(B5d`YVdWRa<$4qB7=PX2`odCE_N7yB$`j=>8Oc-AL-B?Z z3@X+9Ql%n8lp8nt<)VnFzeVf23v1)}*h1>{zC;SXzb`OTZobqbP~?0dVLVbqaf7Hu zgwT1b!ILs|-1F)MC7j5Zi$vUcl`#<;n@GdyD8M57DaDZ=yxqQ%Tw2KKw0r-C@r_}` z33I@9E<@Xx98&re!`|Wi+#q~Z_I7=cl^~Csw;%iT6Fo8a+=#;uGuL6tg%PbxdTkMA z=`Kgxt4?_)sc1ciZ6)un&&KQf8>~&IeN=>IJ98u7{LQQ}2cy zLT1X927(I%2{sEpfL`B_bJ*Uj(Xn;iJbA&QcL|LIBsTtyHjl>r>-ex@QQCci>*?H2x z#c0?2auRLZ5^f-#oRmI%U)@LFGOcKWM(Ywkg8onZ$i%4Cn~sT|GR{zfIh<>93JiBG zjV(8}jVshi+&&$)Dzh~`3ySyw^>7j;H4-|G>u&IdMpK8&E2&vMUPF7I+J1h!XinzQ zhpnv?({7UW>!UBDMo)>0=hsh`+}Q&-L{)kZ=gXt0nbghP?u6LRB;r=@BI{8egvb|3>nGm9~{oRD6uDdX(zAus#dZ}?&$z} zL^jWzh`h*VB4_jmhNBAl<>N2W@7}9}>D@=He=%kzi*^?N9ox*S+IP)& zo(4Zxx*HP2qriv!@I)~4*d@&+Jk#{`L32v_>8+Yp=Zy*H^|FHt+T4(X@eOu7+C4K4 zb8dJLg1Q!6VtB%8Vj$vi1rEOby35qnjn28cdD{*P#mPw|u9t3(PIKWG8U=XT_>H|1 z>`dao(fb8zTZc$MY5lOjB#wTzC0fr?sH5n}+(0-VIhx<^io%!TyCjQ=0)qX3XXaxf z$1s34t3?Pr?L4SHi0P0?4%V|=h{3RK$-mJHPI$e5PE)uE*IEoM-X#a2bw+e$sW&Z+ zEY=}0ED?u!U0#p-+)zVnxzA`RfPMY>3Apw^dvud35O1K@dfud|U1?9+?Whe$#A7OI zva+B$eRLK$!kModHk=9(oD7k**|{F~m4}zD4zSiZd<_HtPh~Wd?d0xBkB&7#+XpV! zp0&Wq*sE%IJFVF&J2UV%Fwmldj*Ist6({Y@oH#Z%n!ANkd0D-vMouJ;cv|{=O`#jH z7vnaK^aiHwFCun)c9xgcK9k*SccV~uyg0Qnj?G;zEa{;3K25-26H!#-vXnTU=Iacj zhL!|QR%MSGNL}2a_VwsqLHS1)kXUFF@$hT zwiB8`x9ox&dDYI6*2EeXDR?Om1Z4Cf6;E>dlQ(pyox8ie)*?rFoclvb2yx5S<73vH zRdcC6&0{{cfb>m{mc-tYf>kk2mA=uJwKUNmf83H!!f^}2O@wY&3wL6#9(GmQJ6uco zOAVG;{k!QHIH&`s`}wIuGhE4$UiRea`~b&B?<*_QwWGHM7OpFqG98TbCTzQ1pSRW6 zRtlL#gxndaH}`VglPUwbFODwSNbtF3;KPGoU|=h&t0G{6@8}}3bI6rTGcfDEAQ425 zDfbh>u(@0uIvQEi)G%%%kE{8u(9Cc8#zpSX1dzceNC(KQe1H#5ux-TNAh%8d{D(|9 zh>RsReS>N{pJisOnu8wd6$%loAscGA9YJ=+lytx)4xdO6b&lWU`9AE4psumNlnRGFCf zix<)vnGE>x0M>#At;)2&;4aA(&B96Pvg|3un9=jaYJwC^v2y{HN*7@?i9ON5g*&gw za9=+ASp3-FOzcDCgySc6YprApm3${hC;cS`qqOrC2D@c;$M#6#-f`LWjt{&vOEFC` zF;^e3r^20GOU>Fi;$<`4*fQGNG3o6;I46vW8C+wGC*G9VsK{tyiDSu)ojBilSdn(< zBjZ8P{z0}Tw)~MgM*{SVQSm~+Sr5THwPbIcx@yD9G4(f0Q6X%v`tF;WYg|{G}cA%;d65f7*GscbD+zl&UA%d8=wlVM8!lEC+x{r@r z)vxVR@TRh|_GSme_-eQ);8oQ}L;VyWU-FOh?bH{J_9@WqBL%G?OZ!LX2StSeZS?Dz z6Sjjf>YdLK^p!^ zvBq79e@RO1VU=bKDJRnsqW584l_JEfgKaZHF0-0bD9)hnA2(vv01OF zVRl_UmP|{7aH_*N=?!#f+S};L=M#6^kSrDlxHY0(SBFXBnY1@2NaF9c(X#A$v8o#C zv2Bblq}`}KsN3Dl_O{Nb4u}`co|N&sT=ZIK;3ct6O$E#Md>V|tOt~2i)*!sHTOACx z-pl!X@Z4lu@J2+gFxlKux<+~fEUv~?qMQ?Qi(^5kS0uHMLg#55bU&8(fRZ;cl5Zs^ zEh3d}oStm_>}$^*9_POs<<5!aV5`K_D3=_RU#Iej~6QD zQ=+WOKKYqPN^8pF3*EwxysDqoGiTpH!&@Qw_3U&oj$3(PEz(ON%L;RtrWZ41LtLc` zs#)ovLu*^gmgdy3f6%THL3=zGYBarF%$j9dD>$ms#f~ttz2`jZZ=c$Pzv6}m!U3qq zZF=i%4#rowlQ3yiLd>i7stY&<&erX3rP<;jXgPcP7CK}g&iEyi>d&aC0(!J)54r{x zN$xYT*d2WuRo)RQyH(}W1l zbXtnaPq%Z!Sj#820xm+_E>4g{mh#96usq+Bt18k-DiLI@=!DVwmBr9jtm-UzB9Vr( zTDu+h#)V%|aPh&zW(R5F$cNRXiRcQL5sJ#Mn;$ubA-;bfz7V@Y4#n$S8MeHQd)i@N z9p$!{73J&vrEtGLS0i|0^X;Jhx(zVISU+!wE@c;XjLqOG=I0L0e7a|cSera=k0@6@c!FHNx6{acu1+D~*ulCX zhCw!=o)Z@dQipV74~m&$VMDqxsU1=)13Zq~nJ3kr(1A=?r5#CuJjMvQvm=g&(?T*~ zP{7oCEBt0>ucSL4N_ni~{Alk2*TI`7WLhpMmO;dh4(obZYKi#tR;?=-*d$un@&Nt3#oSS{U%7B2&ezQ@a-g)fAnYOvL`!1(q#g z$E6$GN2hnWabS_`E2I&+LAdkaTxlG@6qrXs&fy>9$xK zcWaNF9l%KlRYb3SZ=TBT_F zh|zv&1efunJX)rtge#l%X>1JA;ej`r#AN9~Cu?bM1eeX}X2)EV^PPk|)if@Mxr?bZ ziUQtK(=?lIpQsJ{E{&_b-1y9l>U$MOda*e}8y&2LD?7QPX<@jhUnz*KU#|M#cD2I2 zK4s8`oEw#RLSyWq=LeyJ$+zY`h5`wcl^WS?ngf=G@0nP?Rgjo&UAw5XoZP)N*!i6M zY_@QYY`&K5BrjmcS(K|J_p8>Bfg1xEYHMAkJ%0XqWzxn;AYRBkt!j*UT=gUM?JPaL z3+DwZzl+m2!G`7bq5w z(`WXSm6J$%9lLWG8|jYn`w<)I6A9I&JM_on&Kq8H2Ref*HP%-p!C`0~p8Iq4&DqzJ(%5Vgs1Nut~qTRWLJETWAETttyMR+Js~*%g>|8_#M%`Bh3jNk z(!RQSe`IBQk~bpNkM`IirtRs$S1Qe;Nj;5ZWEPwDFTSRECb{Ek5sIQ^6U!T}Y7Dt< zdxyKBacYpnyNLsZ2sxG8nIXkkLVI<$G=|ou%qzFB^v<(|$$7U$cK0x|*{3PU=YPeqX zmHqW-#d*qH!+JckIk#(LC8Pds-h_j3cwDbvzNsXu&V09Ok5?hS7=h!N5GD5j#Hp5U za(7OxU^CA~1ue1o>xC28I}IMSt5uR3nCw{`N_Cc*t9-0-usv>f^qwy(7)WkTY^6_z zn%{`vjSP?*um^WlnFz{##l$FB8*4Zc-&>D{xop+#gcsPNR4jynBC~v z9KkXeY?JNRi+IXTl3c9plNoHpF*ZHYjyfd`+2zk)M7~)kM5y> z7Nd$-nA>tj(;Uy4D8p*pMJ3_c^sT@YSqWBBBz1PK!3a9d4;!r)uGOkeX*Oe)-WFEb zNce|3v26w~Sl^A?6NE8vHdZkr>Czz}SAK=3*ObuZu9O-cZO&T?wP+rZo0ro?7VwyP zv){cpH7mM8E{~;SZx(C`XqyYntu1ZUHyo|uEqi&x!p9vKVp>mz$0pdEa+M2H5wH?h zbrEP7;cru4%wSz8c6_Z!c6n+l@(6x->*55B-PFZe%=F?}9Aa=rwRVGV`?|ZVj`>EL z#+L(%(TeaLGGKanu8GFW^O&4CeRkd+7Mxo6nopL(dcz>iV&c5rW?gt;|Lf=Rnf!*W zjk3@9b1oVva8Q;H16(xXh(%8k91{q;-;Oq|aN3WZa%UpVt!C2{V||>ZoS*lNLsdnn zgUu@)OcXmmHvN3&Fn)qCBbtmX*P!0hMBuJuz(dsmZj-B8A)y=v_kFTjfPQsM}ntpJL~W+>+Qp6&ygWNgSFw`z6d`$2V8 zwI=BY4Ap&H$BSE0z=Crlj%RW^g)3X_PrOsVR(`7!1$|K&-s_rKb++{&G2UaXow*Qu zfWpg(GVk9(b~C--G|7|Va{dAcoffS=vmK)rkD?+P5m;uYyv1X ziZXt){<8tIfjjSZhGR#TkTxRD^-Y_w3P*b#*#{@c^0ew0SmXD!S}NI2H@&tY4&yba zIFI8s4bZih`^a{p5pR91=u`YE!+~_~J@VV)l9IV6+(OCuIzlPMCKX5zoZ=!GMpQgO zyjz5L(|ha>AZw*`tmKD+b9;4!6cBe9b5-3>6s83-B8W;EPgEYw9gpY)j&HlLS&N`k zHDzrVGgE-H_vC9Olg*WGroJYHCeFLvMtbh_ijB~c3DtSeC?*s01yWH6ZKt(`nB-^g zjd0OW)HStrlphPrkCQPWPdwT6qQ;oHf?}r`)xtA;*^BC{J<3gS;pWb#`@6X%>cIy*|nL1;bmNJC((4aML{VO7G2lg?$d=F;LX6-g8Ec|8( zyTWQS}kIxwb}DbE~hGe9=QbyUtq@g0aBjO}=HOz&soURjJ1MYhxJH+9IKZ6Z*@6ez0`B8Z6v9-N2(&i($S zX>S5a5Hj*y87pO;Xei!n5*{p-X;=6$2k`92G-K&Bt1~%3a3d9-RkNGA2OE6Co@4&9 z^-K|LpJJ3>xh|ZF{rHkn*@E0*y^KU}6j=!se@r@OwVC*qIP*9oJyK(AJGO?ODCP@Y zmvQ%t&w8UQFfLbP5_amGHRNogACu${W$}z9>UyB*TN#P+%DL5(Jz#? z9j)T#bb-yz2an(s!gFfQu|M>=SKXddslptnu0sb}EN1pKkh4I~EjiXEARvX!6B$=P%B0q#x=d`UZv2|t zVnS2zmk&EVbx8JKLi^3;Fg5zFN`Ryz)PriK5A?3BK)DVC)F68ban@OyH3N6j@fg!j z#xTLHe>=81)M%s1+eSaM|HSFhH(%FZx>q>{4~18uJ5befx%BufX22J#h_am@68FT| za|HpTL$;;Xcl#Acz12xFmO9sC7TU{oa$T3fEkz^iUm zJSFWMfN6Sy(C=j1l@XD~3G)hDUGaXV{Ym%9-7Y@ap0P5?n)KQQU|h%*WF|%f0FH^s zK!1@@;c|s|KG&0~OGBqxUnm)*Mv~QiEieie;gqrIycf>4<^+=S?W#&S!>ADZ(dHYM z?ft9L*0w#8S^<%_Z8kUGl6>nY=QF}rl3Tqc8^>4VbhX4IU-Gx)`5bx2y1S`&Xk$m9 zalD*-lG_bJ{CZZ0cX~Icol}wmoaC3dUus>bxxG|Dc?%5oCwSIhN;}@?;9Dl5C0cnZ z7tTYj^fd?bF+X63e=e=?a{fyh4m69-(9G@a5SmX6L-!?jMNZI zv18Kk)^dEle{KY9pLo4}84RM0FIE>`W1vl%D%X|n6CbAh@DcslC8tClQ8Z&{u5d3# z;HDl+>Jr0z3Vo7y)lK4q>W9kv)dzjpw+Xw{30qMGrC!L=xHSk)@e=1j5j7T7vM$Uz z=SrcIC?}Wu>!lJ`GQw9&IvU}wy}A6;Db7RH(s|N(`TeB;5cC8M)7D1$AK>*D0?SutYd*cC#}6 zUAK5TZ*OvnbUM=Ee%nEIIpA(XV-I3?%ZZIa5UB0K*Vlh({`bm9pVc0ak3Q??Lb()j z2{N+rUW9!CQk;C*qH7r~2ohRz5XgCXOm+|H2>MmmfkDHXq>T8cxQ<_S0K8WonVehcO#%Hs z1p4Q1KHiaX%cJ0mxs>cLlqeHDC`10=Y6;5d|F^a@_hmjCzW?W||IsROl;%ETU&-GH z$CDDlSpKH=&ELtT9l37=7*I>ObbstQ|9tQ?qm^nii@K%#o8bbNMab_AnUd0eQxOjd zeCsWRPp0UgF?ps@{aqk`%7KRoEbsj+INx@Wf4<}L8(>VR zsH&=BjN5+Sf`9(LyfsAQGP4uu_rIFH|EUT9AO44`XlQ6g-R=_qdNn{sH+;skR!6#N z^P5(s3-TP*{p-MQdx=jieY=(YdDo=;J;eT9eZ-*u+LlxWrVhb}zZMYaQR=@cp;VUs z{iNT@MJ;{+d>kw|f3uh(%MnVoL0`SN`RgqkjJze-6C2ld|8d0p^Y!V^frNeIQThH< zslU&|u2PR}FaOac;a?Y#3g&@=1bm4JH-A$PPdvS+im0fl4(A@xuVqLm6%0n}Ut1*h zul6M;F$>)xXh5J zXup}opTR6f?OpoM_0UiDP^oYpDEMUNzyGa>A9|{oquTcQ{CyDrB5)pgcp?>fVI1#Y zmx+G?7TmW>HosFG5gw^5N~767Rp}p%W~7!f0iV*sfS=FH?`_INPoGn0U}^lv&;L?$ zSKc@Mn(p}BV40-xRF7`uOiB;J=PuI)8kGD#=ys*T3$!2Tu}EF#pyi zp`R=mxSCt^SFXU5UgiS$QH9h8AnkBzoOi#jGF}?^J#fN%1AkNGiKhzKn)JM|e^ZYK z4>5dk1V9%bFa1WIJn>J!$o*@JKK!bWfAot7S`d7b|0h!TPdDOu!92S*ZA1K*>G_}k z{rFkB1ej-RCd9uP6h?Ajz7ahFQ9RZ{wl)74Q)jkl@dGruFLo E03AYKNdN!< literal 0 HcmV?d00001 diff --git a/variables.tf b/variables.tf index 490abdb..083a385 100644 --- a/variables.tf +++ b/variables.tf @@ -13,24 +13,31 @@ variable "deploy_demo" { type = bool description = "Deploys a simple demo using a global IP as ingress and a hello-kubernetes pods" default = false + validation { + condition = !var.deploy_demo || var.global_ip + error_message = "When deploy_demo is true, global_ip must be true as well." + } } variable "clusters" { - description = "K3s cluster definition" + description = "Cluster definition" type = list(object({ - name = optional(string, "K3s demo cluster") + name = optional(string, "Demo cluster") metro = optional(string, "FR") plan_control_plane = optional(string, "c3.small.x86") plan_node = optional(string, "c3.small.x86") node_count = optional(number, 0) - k3s_ha = optional(bool, false) + ha = optional(bool, false) os = optional(string, "debian_11") - control_plane_hostnames = optional(string, "k3s-cp") - node_hostnames = optional(string, "k3s-node") - custom_k3s_token = optional(string, "") + control_plane_hostnames = optional(string, "cp") + node_hostnames = optional(string, "node") + custom_token = optional(string, "") ip_pool_count = optional(number, 0) - k3s_version = optional(string, "") + kube_version = optional(string, "") metallb_version = optional(string, "") + rancher_flavor = optional(string, "") + rancher_version = optional(string, "") + custom_rancher_password = optional(string, "") })) default = [{}] } diff --git a/versions.tf b/versions.tf index 3cc65f8..3050f56 100644 --- a/versions.tf +++ b/versions.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.3" + required_version = ">= 1.9" required_providers { equinix = { source = "equinix/equinix"