From c2af5a6c43b3da7b8dbb15674fa4734c49990854 Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Tue, 5 Nov 2024 00:06:56 -0600 Subject: [PATCH] feat: add a talos blog post for openstack-flex This change adds a new blog post for Talos, which I'll be using for content in other application centric posts being developed for GA. > Updates the CSS to have a better reading experience on long content. Related-Issue: https://rackspace.atlassian.net/browse/OSPC-118 Signed-off-by: Kevin Carter --- ...4-11-04-running-talos-on-openstack-flex.md | 427 ++++++++++++++++++ .../assets/images/2024-11-04/talos-logo.png | Bin 0 -> 11593 bytes docs/overrides/stylesheets/adr.css | 13 +- 3 files changed, 429 insertions(+), 11 deletions(-) create mode 100644 docs/blog/posts/2024-11-04-running-talos-on-openstack-flex.md create mode 100644 docs/blog/posts/assets/images/2024-11-04/talos-logo.png diff --git a/docs/blog/posts/2024-11-04-running-talos-on-openstack-flex.md b/docs/blog/posts/2024-11-04-running-talos-on-openstack-flex.md new file mode 100644 index 0000000..18aabff --- /dev/null +++ b/docs/blog/posts/2024-11-04-running-talos-on-openstack-flex.md @@ -0,0 +1,427 @@ +--- +date: 2024-11-04 +title: Running Talos on OpenStack Flex +authors: + - cloudnull +description: > + Running Talos on OpenStack Flex +categories: + - Operating System + - Image + - Server + - Kubernetes +--- + +# Running Talos on OpenStack Flex + +![talos-linux](assets/images/2024-11-04/talos-logo.png){ align=left } + +As developers, we're constantly seeking platforms that streamline our workflows and enhance the performance and reliability of our applications. Talos is a container optimized Linux distribution reimagined for distributed systems. Designed with minimalism and practicality in mind, Talos brings a host of features that are particularly advantageous for OpenStack environments. By stripping away unnecessary components, it embodies minimalism, reducing the attack surface and resource consumption. It comes secure by default, providing out-of-the-box secure configurations that alleviate the need for extensive hardening. Additionally, it is managed via a single declarative configuration file and gRPC API, simplifying management and automation to fit seamlessly into modern DevOps practices. Talos can be deployed across containers, cloud platforms, virtualized environments, and bare-metal servers, offering versatility in how you manage your infrastructure. + + + +Integrating Talos with OpenStack Flex brings significant benefits. The immutable and minimal nature of Talos ensures that all compute nodes in your OpenStack cluster are consistent, reducing the chances of unexpected behavior due to environmental differences and thus enhancing consistency and reliability. When OpenStack Flex and Talos are combined, it creates an optimal environment for developers. Talos's ephemeral and atomic nature makes scaling out compute resources in OpenStack Flex seamless and efficient, enhancing scalability. The combination ensures high availability and quick recovery from failures, as Talos's design simplifies node replacement and recovery, thereby improving resiliency. + +In essence, Talos offers more by providing less—less complexity, less overhead, and fewer security concerns. This minimalistic yet powerful approach enhances security, efficiency, resiliency, and consistency. For developers working with OpenStack and specifically OpenStack Flex, Talos presents a compelling operating system choice that aligns perfectly with the goals of modern open infrastructure native applications. + +## Creating a cluster via the CLI on OpenStack + +In this guide, we will create an HA Kubernetes cluster in OpenStack with 3 worker node. We will assume an existing some familiarity with OpenStack. If you need more information on OpenStack specifics, please see the official OpenStack documentation. + +``` mirmaid +flowchart TD + A[Internet] --> |Floating IP| B(Neutron Router) + B --> |Neutron Network| C{OVN Loadbalancer} + B --> |Floating IP| X[Jump 0] + C -->D[Controller 0] + D <--> G[Worker 0] + D <--> H[Worker 1] + D <--> I[Worker 2] + C -->E[Controller 1] + E <--> G[Worker 0] + E <--> H[Worker 1] + E <--> I[Worker 2] + C -->F[Controller 2] + F <--> G[Worker 0] + F <--> H[Worker 1] + F <--> I[Worker 2] + X <--> D + X <--> E + X <--> F +``` + +### Environment Setup + +You should have an existing openrc file. This file will provide environment variables necessary to talk to your OpenStack cloud. See here for instructions on fetching this file. + +## Create the Image + +First, download the OpenStack image from a [Talos Image Factory](https://factory.talos.dev). + +!!! example "At the time of this writing the latest image was 1.8.2" + + ``` shell + wget https://factory.talos.dev/image/376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba/v1.8.2/openstack-amd64.raw.xz + ``` + +Once the image is downloaded, decompress the file. + +``` shell +xz --decompress -v openstack-amd64.raw.xz +``` + +After decompressing the file downloaded, the command will result in a raw image file named, `openstack-amd64.raw`. + +### Upload the Image + +Once you have the image ready, you can upload to OpenStack with the following command + +``` shell +openstack --os-cloud default image create \ + --progress \ + --disk-format raw \ + --container-format bare \ + --file openstack-amd64.raw \ + --property hw_vif_multiqueue_enabled=true \ + --property hw_qemu_guest_agent=yes \ + --property hypervisor_type=kvm \ + --property img_config_drive=optional \ + --property hw_machine_type=q35 \ + --property hw_firmware_type=uefi \ + --property os_require_quiesce=yes \ + --property os_type=linux \ + --property os_admin_user=talos \ + --property os_distro=talos \ + --property os_version=18.2 \ + talos-18.2 +``` + +This command will prepare the image to run in a KVM environment, with UEFI firmware, and the Talos operating system. The image will be named `talos-18.2`. + +## Network Infrastructure + +The network setup will cover the creation of a router, network, subnet, load balancer, and ports. + +!!! note "This blog post was written with the following environment assumptions already existing" + + - Router: `tenant-router` + - Network: `tenant-net` + - Subnet: `tenant-subnet` + - Key Pair: `tenant-key` + + If you don't have a setup project, checkout the getting started guide [here](https://blog.rackspacecloud.com/blog/2024/06/18/getting_started_with_rackspace_openstack_flex). + +### Creating loadbalancer + +The OpenStack Flex Loadbalancer used for this environment is a Layer 4 TCP load balancer powered by the OVN loadbalancer solution. The Loadbalancer will be used to distribute traffic to the control plane nodes. + +!!! tip "Check the loadbalancer providers available in your environment" + + ``` shell + openstack --os-cloud default loadbalancer provider list + ``` + +Create load balancer, updating vip-subnet-id if necessary + +``` shell +openstack --os-cloud default loadbalancer create --provider ovn --name talos-control-plane --vip-subnet-id tenant-subnet +``` + +Store the load balancer ID for later use + +``` shell +LB_ID=$(openstack --os-cloud default loadbalancer show talos-control-plane -f value -c id) +``` + +Create listener + +``` shell +openstack --os-cloud default loadbalancer listener create --name talos-control-plane-listener --protocol TCP --protocol-port 6443 talos-control-plane +``` + +Create Pool + +``` shell +openstack --os-cloud default loadbalancer pool create --name talos-control-plane-pool --lb-algorithm SOURCE_IP_PORT --listener talos-control-plane-listener --protocol TCP +``` + +Create health monitoring + +``` shell +openstack --os-cloud default loadbalancer healthmonitor create --delay 5 --max-retries 4 --timeout 10 --type TCP talos-control-plane-pool +``` + +## Security Groups + +Secrutiry groups allow you to control the traffic to and from your instances. We will create two security groups, one for the tenant and one for the Talos control plane. + +### Create a tenant security group + +Craete a tenant security group, this tenant-secgroup will be used to permit SSH traffic to your jump host. + +``` shell +openstack --os-cloud default security group create tenant-secgroup +``` + +Add an SSH rule to the security group, allowing traffic from anywhere. + +``` shell +openstack --os-cloud default security group rule create tenant-secgroup \ + --protocol tcp \ + --ingress \ + --remote-ip 0.0.0.0/0 \ + --dst-port 22 +``` + +### Create a Talos control plane security group + +The security group will be used to permit traffic to the control plane nodes. We will open the following ports: + +Craete a Talos security group, this `talos-secgroup` will be used to permit Talos control plane and kubernetes traffic within the cluster. + +``` shell +openstack --os-cloud default security group create talos-secgroup +``` + +Add the Talos control plane security group rules. + +* `6443` for the Kubernetes API server +* `50000` for the Talos API + +``` shell +openstack --os-cloud default security group rule create --ingress --protocol tcp --dst-port 6443 talos-secgroup +openstack --os-cloud default security group rule create --ingress --protocol tcp --dst-port 50000 talos-secgroup +openstack --os-cloud default security group rule create --ingress --protocol tcp talos-secgroup +openstack --os-cloud default security group rule create --ingress --protocol udp talos-secgroup +``` + +### Creating ports + +Create ports used for IP address allocation for the control plane and jump nodes. + +``` shell +JUMP_0=$(openstack --os-cloud default port create --security-group tenant-secgroup --security-group talos-secgroup --network tenant-net jump-0 -f json | jq -r '.fixed_ips[0].ip_address') +CONTROLLER_0=$(openstack --os-cloud default port create --security-group talos-secgroup --network tenant-net talos-control-plane-0 -f json | jq -r '.fixed_ips[0].ip_address') +CONTROLLER_1=$(openstack --os-cloud default port create --security-group talos-secgroup --network tenant-net talos-control-plane-1 -f json | jq -r '.fixed_ips[0].ip_address') +CONTROLLER_2=$(openstack --os-cloud default port create --security-group talos-secgroup --network tenant-net talos-control-plane-2 -f json | jq -r '.fixed_ips[0].ip_address') +``` + +!!! note + + The jump-0 port has both the `tenant-secgroup` and `talos-secgroup` security groups. The control plane ports have only the `talos-secgroup` security group. + + The above commands will store the port IP addresses in the variables `JUMP_0`, `CONTROLLER_1`, `CONTROLLER_2`, and `CONTROLLER_3`. These variables will be used in the next step. You can validate the variables are defined and have the correct values by running `echo $JUMP_0 $CONTROLLER_1 $CONTROLLER_2 $CONTROLLER_3`. + +### Associate port’s private IPs to loadbalancer + +Create the loadbalancer members for each port IP. + +``` shell +openstack --os-cloud default loadbalancer member create --subnet-id tenant-subnet --address ${CONTROLLER_0} --protocol-port 6443 talos-control-plane-pool +openstack --os-cloud default loadbalancer member create --subnet-id tenant-subnet --address ${CONTROLLER_1} --protocol-port 6443 talos-control-plane-pool +openstack --os-cloud default loadbalancer member create --subnet-id tenant-subnet --address ${CONTROLLER_2} --protocol-port 6443 talos-control-plane-pool +``` + +### Associate floating IPs to the `jump-0` port + +Create a floating IP for the jump host. + +``` shell +openstack --os-cloud default floating ip create --port jump-0 PUBLICNET +``` + +Retrieve the floating IP for the jump host. + +``` shell +JUMP_PUBLIC_VIP=$(openstack --os-cloud default floating ip list --fixed-ip-address $JUMP_0 -f json | jq -r '.[0]."Floating IP Address"') +``` + +### Associate floating IPs to the loadbalancer port (optional) + +!!! note + + This is an optional step, if you never want to access the kubernetes API over the public internet, you can skip this step. + +Create a floating IP for the load balancer. + +``` shell +openstack --os-cloud default floating ip create --port ovn-lb-vip-${LB_ID} PUBLICNET +``` + +Retrieve the floating IP for the load balancer. + +``` shell +LB_PRIVATE_VIP=$(openstack --os-cloud default loadbalancer show talos-control-plane -f json | jq -r .vip_address) +LB_PUBLIC_VIP=$(openstack --os-cloud default floating ip list --fixed-ip-address ${LB_PRIVATE_VIP} -f json | jq -r '.[0]."Floating IP Address"') +``` + +!!! tip + + This setup is making the assumption that you will be running `talosctl` from the jump host. If you indend to run `talosctl` from outside the OpenStack environment, and you want direct access to all of the control plane nodes, you will need to create floating IPs for the nodes. + + ``` shell + openstack --os-cloud default floating ip create --port talos-control-plane-0 PUBLICNET + openstack --os-cloud default floating ip create --port talos-control-plane-1 PUBLICNET + openstack --os-cloud default floating ip create --port talos-control-plane-2 PUBLICNET + ``` + +## Build the Jump Host + +The jump host will be used to interact with the Talos cluster. We will use the floating IP we created earlier to access the jump host. + +``` shell +openstack --os-cloud default server create jump-0 --flavor gp.0.1.2 \ + --nic port-id=jump-0 \ + --image Debian-12 \ + --key-name tenant-key +``` + +Login to the jump host and install `talosctl`. + +``` shell +ssh debian@${JUMP_PUBLIC_VIP} 'curl -sL https://talos.dev/install | sh' +``` + +!!! tip + + See the Talos install [documentation](https://www.talos.dev/v1.8/talos-guides/install/talosctl/) for more information on installing `talosctl`. + +## Cluster Configuration + +With our networking deployed, and the jump host online fetch the IP for our OVN Loadbalancer. + +Generate the configuration. + +``` shell +ssh debian@${JUMP_PUBLIC_VIP} "talosctl gen config talos-k8s-openstack https://${LB_PUBLIC_VIP}:6443" +``` + +Upon the completion of this command the local directory will contain a `talosconfig`, `controlplane.yaml`, `worker.yaml` files. This file will be used to interact with the Talos cluster. + +Retrieve these files from the jump host to the machine running the OpenStack CLI. + +``` shell +scp debian@${JUMP_PUBLIC_VIP}:talosconfig . +scp debian@${JUMP_PUBLIC_VIP}:controlplane.yaml . +scp debian@${JUMP_PUBLIC_VIP}:worker.yaml . +``` + +## Talos Compute Creation + +To build the Talos cluster, we will create the control plane nodes and worker nodes. We will use the `controlplane.yaml` and `worker.yaml` files we retrieved from the jump host. + +### Create control plane nodes + +!!! tip + + The following command will create 3 control plane nodes. You can adjust the number of control plane nodes by changing the `seq` range. + + Depeding on where your command is you may need to retrieve the `controlplane.yaml` file from the jump host. + +``` shell +for i in $(seq 0 1 2); do + openstack --os-cloud default server create \ + talos-control-plane-$i \ + --flavor gp.0.2.4 \ + --nic port-id=talos-control-plane-$i \ + --image talos-18.2 \ + --key-name tenant-key \ + --user-data controlplane.yaml +done +``` + +### Create worker nodes + +!!! tip + + The following command will create 3 control plane nodes. You can adjust the number of control plane nodes by changing the `seq` range. + + Depeding on where your command is you may need to retrieve the `worker.yaml` file from the jump host. + +``` shell +for i in $(seq 0 1 2); do + openstack --os-cloud default server create \ + talos-worker-$i \ + --flavor gp.0.2.4 \ + --network tenant-net \ + --image talos-18.2 \ + --key-name tenant-key \ + --user-data worker.yaml +done +``` + +!!! note + + The above command will create a set of 3 workers. However there's no limit. You can add workers following this process whenever is needed. + +### Bootstrap Etcd + +You should now be able to interact with your cluster with talosctl from the jump host. We will the floating IPs we retrieved earlier to bootstrap Etcd. + +Return to the jump host and set the endpoints and nodes. + +### Set the endpoints and nodes + +``` shell +ssh debian@${JUMP_PUBLIC_VIP} "talosctl --talosconfig talosconfig config endpoint ${CONTROLLER_0}" +ssh debian@${JUMP_PUBLIC_VIP} "talosctl --talosconfig talosconfig config node ${CONTROLLER_0}" +``` + +### Bootstrap etcd + +``` shell +ssh debian@${JUMP_PUBLIC_VIP} "talosctl --talosconfig talosconfig bootstrap" +``` + +### Install the kubectl binary + +At this stage login to the jump host and install the `kubectl` binary + +``` shell +ssh debian@${JUMP_PUBLIC_VIP} +``` + +Install the `kubectl` binary is optional but an easy way to interact with your Talos environment now that it is deployed. + +``` shell +curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" +``` + +Move the binary to a location in your path. + +``` shell +sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl +``` + +At this point we can retrieve the admin kubeconfig by running + +``` shell +talosctl --talosconfig talosconfig kubeconfig . +``` + +Check your nodes + +``` shell +./kubectl --kubeconfig ./kubeconfig get nodes +``` + +!!! example "The output should look something like this" + + ``` shell + NAME STATUS ROLES AGE VERSION + talos-control-plane-0 Ready control-plane 2m6s v1.31.2 + talos-control-plane-1 Ready control-plane 2m2s v1.31.2 + talos-control-plane-2 Ready control-plane 2m5s v1.31.2 + talos-worker-0 Ready 118s v1.31.2 + talos-worker-1 Ready 112s v1.31.2 + talos-worker-2 Ready 112s v1.31.2 + ``` + +Assuming the output of the command is matching your expectations for the deployment you ran following this blog post, you have successfully deployed a Talos cluster on OpenStack Flex. +With the cluster up and running, you can now deploy your applications and services on top of it all from the jump host. + +## Conclusion + +To recap, this blog post has outlined the steps to deploy a Talos cluster on OpenStack Flex. The cluster consisted of three control plane nodes and three worker nodes. We created the necessary network infrastructure, security groups, and ports to support the cluster. We then built the jump host and retrieved the necessary configuration files to interact with the cluster. We bootstrapped the cluster using `talosctl`. Finally, we retrieved the admin kubeconfig and installed the `kubectl` binary to interact with the cluster. + +Talos is a powerful operating system that is well-suited for OpenStack Flex environments. By combining the minimalistic and secure nature of Talos with the flexibility and scalability of OpenStack Flex, you can create a robust and reliable infrastructure for your applications. With the steps outlined in this guide, you can easily deploy a Talos cluster on OpenStack Flex and take advantage of the benefits that both platforms offer. Whether you are running a small development environment or a large production deployment, Talos and OpenStack Flex provide the tools you need to build and manage your infrastructure effectively. diff --git a/docs/blog/posts/assets/images/2024-11-04/talos-logo.png b/docs/blog/posts/assets/images/2024-11-04/talos-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..185baef20cf90c0f88c330aae1a2ef2ac6f4d408 GIT binary patch literal 11593 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 { - color: #eb0000; - font-size: .64rem; - padding: .75em .8rem; - transition: color .25s,background-color .25s - } } [data-md-color-accent=red] {