Orchestrating k3s Clusters via Terraform Infrastructure as Code

The intersection of lightweight Kubernetes distributions and Infrastructure as Code (IaC) represents a pivotal shift in how modern engineering teams deploy container orchestration. At the center of this shift is k3s, a highly available and certified Kubernetes distribution that strips away the bloat of traditional k8s to provide a streamlined, production-ready experience. When paired with Terraform, a tool designed for building, changing, and versioning infrastructure safely and efficiently, the process of spinning up a fully functional cluster moves from a manual, error-prone series of steps to a repeatable, version-controlled software engineering process.

The traditional Kubernetes (k8s) installation process is notoriously complex, often requiring a meticulous separation of concerns. A production-ready k8s environment typically demands separate master nodes to run the control plane, worker nodes to execute the actual workloads, and a dedicated cluster for the etcd database to ensure High Availability. Furthermore, an ideal setup requires a separate node specifically for the Ingress Controller to manage incoming traffic efficiently. For a developer, replicating this architecture across local development, staging, and production environments creates an immense operational burden.

k3s solves this by packaging the entire Kubernetes distribution into a single binary of less than 40MB. By reducing dependencies and simplifying the architectural requirements, k3s allows for rapid deployment on a variety of platforms, ranging from massive cloud providers like Google Cloud Platform (GCP) to resource-constrained hardware like Raspberry Pi. This accessibility makes it an ideal choice for Internet of Things (IoT) and edge computing use cases, where low footprint and high efficiency are non-negotiable.

The k3s Distribution Paradigm

k3s is not merely a "small" version of Kubernetes; it is a certified distribution designed with simplicity at its core. Since its launch in early 2019, it has gained significant traction within the Cloud Native Computing Foundation (CNCF) Landscape, boasting nearly 14k GitHub stars and being recognized as the number one new developer tool of 2019 by Stackshare.

The primary value proposition of k3s lies in its reduced resource requirements. By eliminating unnecessary legacy drivers and optimizing the internal components, k3s reduces the time it takes to set up a production-ready cluster. This allows developers to maintain parity between their local machines and their remote cloud instances, eliminating the "it works on my machine" syndrome.

Terraform as the Infrastructure Foundation

Terraform serves as the orchestration layer that enables the automated creation of the virtual machines (VMs) that will eventually host k3s. As the most popular player in the IaC space, Terraform allows administrators to define their entire network and compute stack in a declarative language. Instead of clicking through a cloud console, a developer writes a configuration file that describes the desired end state of the infrastructure.

In the context of k3s, Terraform is used to provision the Google Compute Engine (GCE) instances, configure the necessary network interfaces, and establish the firewall rules that allow the Kubernetes control plane to communicate with the worker nodes. By versioning these files in a Git repository, teams can track every change to their cluster's infrastructure, enabling rapid rollbacks and consistent scaling.

Critical Network Configuration for k3s

A Kubernetes cluster cannot function if the nodes cannot communicate. For a k3s deployment on Google Cloud, a specific firewall rule must be established to allow traffic on the ports required by the Kubernetes API server.

Specifically, port 6443 must be open to allow the communication between the master node and the worker nodes, as well as for the local administrator to interact with the cluster via the command line. To implement this in Terraform, a google_compute_firewall resource is created. This rule is applied specifically to instances that carry the k3s tag, ensuring that only the relevant nodes are exposed to this traffic.

hcl resource "google_compute_firewall" "k3s-firewall" { name = "k3s-firewall" network = "default" allow { protocol = "tcp" ports = ["6443"] } target_tags = ["k3s"] }

The impact of this configuration is the creation of a secure perimeter. By using target_tags, the security posture is tightened, ensuring that the firewall rule does not open port 6443 on every single VM in the project, but only on those explicitly marked as part of the k3s cluster.

Provisioning the Master Node

The master node is the brain of the cluster, running the control plane that manages the state of the entire system. In a Terraform-based deployment, the master node is defined as a google_compute_instance.

The selection of the machine type (e.g., n1-standard-1) and the boot disk image (e.g., debian-9-stretch-v20200805) determines the baseline performance and stability of the control plane. The master node must be tagged with both k3s (to inherit the firewall rules) and k3s-master (for identification purposes).

To move from a blank VM to a functional Kubernetes master, Terraform utilizes a local-exec provisioner. This is a critical distinction in Terraform's operational model: while a remote-exec provisioner runs a script on the target VM, a local-exec provisioner runs a command on the machine where Terraform is being executed.

In this scenario, local-exec is used to trigger k3sup, a tool specifically designed to simplify the installation of k3s. The command structure leverages Terraform's interpolation to pass the dynamic IP address of the newly created VM to the installation tool.

hcl resource "google_compute_instance" "k3s_master_instance" { name = "k3s-master" machine_type = "n1-standard-1" tags = ["k3s", "k3s-master"] boot_disk { initialize_params { image = "debian-9-stretch-v20200805" } } network_interface { network = "default" access_config {} } provisioner "local-exec" { command = <<EOT k3sup install \ --ip ${self.network_interface[0].access_config[0].nat_ip} \ --context k3s \ --ssh-key ~/.ssh/google_compute_engine \ --user $(whoami) EOT } depends_on = [ google_compute_firewall.k3s-firewall, ] }

The use of depends_on is vital here. It creates a logical dependency ensuring that the firewall rule is fully active before Terraform attempts to run the k3sup installation. If the firewall were not ready, the SSH connection used by k3sup would be rejected, leading to a deployment failure.

Leveraging k3sup for Rapid Deployment

k3sup acts as the bridge between the raw infrastructure provided by Terraform and the operational state of a Kubernetes cluster. It is a specialized utility that automates the process of installing k3s via SSH, meaning it does not require an agent to be pre-installed on the target VM.

The installation of k3sup is performed via a simple shell command:

bash curl -sLS https://get.k3sup.dev | sh sudo install k3sup /usr/local/bin/ k3sup version

Once installed, k3sup provides several high-value capabilities that extend beyond simple installation:

  • Fetching the KUBECONFIG: It can automatically retrieve the configuration file from the remote k3s cluster and save it to the local machine. This allows the user to use kubectl immediately without manual SSH file transfers.
  • HA and Multi-Master Support: While basic setups use a single master, k3sup has the capacity to build High Availability clusters with multiple master nodes.
  • Worker Node Integration: The k3sup join command allows for the rapid addition of worker nodes to an existing cluster.

Deep Dive into Terraform Provisioners

Provisioners are the tools Terraform uses to initialize a server after it has been created. While Terraform is primarily an infrastructure tool and not a configuration management tool (like Puppet or Chef), provisioners allow for the "bootstrapping" of a server.

Remote-Exec Provisioner

The remote-exec provisioner allows Terraform to connect to the remote machine via SSH and execute a script or command. This is often used for installing system packages or running an official installation script.

A typical remote-exec block requires a connection block to specify the user and the host IP. A sophisticated approach involves using the path.module construct, which tells Terraform to search for the script relative to the directory where the module is located, ensuring the code remains portable across different environments.

hcl provisioner "remote-exec" { script = "${path.module}/scripts/bootstrap.sh" connection { user = "root" host = "${self.access_public_ipv4}" } }

In this context, the self keyword is used to reference the attributes of the resource being declared. This allows the provisioner to dynamically identify the public IP address of the instance it is currently configuring.

Local-Exec Provisioner

In contrast, the local-exec provisioner runs commands on the machine where the Terraform binary is executing. This is essential for tasks that must happen locally, such as updating a local .kube/config file or triggering a local API call to notify a monitoring system that a new node is online.

The combination of remote-exec (to prepare the server) and local-exec (to configure the local client) creates a complete end-to-end deployment pipeline.

Execution and Validation Workflow

The deployment process follows a strict sequence of Terraform CLI commands. Each step serves as a validation gate to ensure the infrastructure is being built as intended.

  1. Initializing the environment:
    terraform init
    This command downloads the necessary provider plugins (such as the Google Cloud provider) and initializes the backend.

  2. Planning the deployment:
    terraform plan
    This creates an execution plan, allowing the operator to see exactly which resources will be created, modified, or destroyed.

  3. Applying the configuration:
    terraform apply
    This executes the plan, provisions the GCE instances, opens the firewall, and triggers the k3sup installation.

Once the terraform apply command completes, the local-exec provisioner saves the kubeconfig file locally. This file is the key to controlling the cluster. By pointing kubectl to this file, the operator can verify the health of the nodes.

The output of a successful 4-node cluster verification looks like this:

bash kubectl get nodes

NAME STATUS ROLES AGE VERSION
k3s-master Ready master 103s v1.18.6+k3s1
k3s-worker-0 Ready 43s v1.18.6+k3s1
k3s-worker-1 Ready 10s v1.18.6+k3s1
k3s-worker-2 Ready 42s 10s v1.18.6+k3s1

Comparative Analysis: k3s vs. Standard k8s

The decision to use k3s over standard Kubernetes is usually driven by the need for reduced operational overhead and lower resource consumption.

Feature Standard k8s k3s
Binary Size Multiple binaries / complex Single < 40MB binary
Dependency Level High (many system dependencies) Low (stripped down)
Setup Complexity High (manual control plane setup) Low (automated/simplified)
Ideal Use Case Large-scale Data Centers Edge, IoT, Staging, Dev
Resource Footprint Heavy Lightweight
Certification CNCF Certified CNCF Certified

The impact of these differences is most felt during the "Day 0" operations (installation). Where a standard k8s cluster might take hours or days to properly architect with separate etcd and master nodes, a k3s cluster can be provisioned in minutes using the Terraform and k3sup workflow.

Advanced Bootstrap Strategies

While shell scripts are the most traditional way to bootstrap a server, there are alternative methods that offer different trade-offs in terms of speed and immutability.

  • Immutable Infrastructure: Instead of using provisioners to install software on a running VM, one can create a pre-configured machine image (a Golden Image) that already contains k3s. Terraform then simply deploys instances of that image. This eliminates the time spent waiting for curl and install scripts to run.
  • Cloud Native Tools: Tools like Ignition can be used to configure a server during the very first boot process, providing a faster and more reliable alternative to SSH-based provisioning.
  • Configuration Management: For highly complex environments, Terraform can be used to trigger Puppet or Chef, which provide more robust state management for the software running inside the VM.

Despite these alternatives, the remote-exec and local-exec method remains the most accessible entry point for developers and is highly effective for most k3s use cases.

Conclusion: The Synergy of IaC and Lightweight Orchestration

The integration of k3s and Terraform represents a sophisticated approach to cloud-native infrastructure. By abstracting the hardware layer through Terraform and simplifying the orchestration layer through k3s, organizations can achieve a level of agility that was previously impossible with standard Kubernetes.

The technical synergy is found in the way Terraform's lifecycle management handles the "where" and "how" of the server's existence, while k3sup handles the "what" of the server's function. This decoupling allows for a highly modular architecture where the underlying cloud provider can be swapped—moving from Google Cloud to another provider or even to on-premise Raspberry Pi clusters—without fundamentally changing the deployment logic.

Ultimately, this workflow reduces the cognitive load on the engineer. Instead of managing the intricacies of etcd databases and control plane certificates, the engineer focuses on the application logic. The result is a production-ready, certified Kubernetes environment that can be destroyed and recreated with a single command, ensuring that the infrastructure is always clean, consistent, and documented as code.

Sources

  1. NimbleHQ - Provision k3s on Google Cloud with Terraform and k3sup
  2. mkdev - Creating Kubernetes Clusters with Terraform: Learning Provisioners

Related Posts