- Part 1
- Vagrant
- K3s
- What Is K3S
- K3S Architecture
- Single-server Setup with an Embedded DB and High-Availability K3s
- How Agent Node Registration Works
- K3S Installation
- Connecting K3S Worker Node to Server Node
- Side Notes
- Further
- Vagrantfile: Configuration file for the environment.
- Vagrant Box: Base image for virtual machines.
- Provider: The virtualization backend (e.g., VirtualBox).
Vagrant.configure("2") do |config|
# Box
config.vm.box = "box name"
config.vm.box_version = "box version"
# Provisioning
# runs a provision using a predefined file
config.vm.provision "shell", name: "install-dependencies", path: "instal-dependendcies.sh"
# runs inline shell provision, the provision below will not `run` when running `vagrant up`
# an be rerun with vagrant provision --provision-with inline-provision
config.vm.provision "shell", name: "inline-provision", run: "never", inline: <<-SHELL
docker compose restart
SHELLVagrant is a tool by Hashicorp for isolating development environments, enabling teams to collaborate efficiently.
- Providers: Hypervisors such as VirtualBox, VMWare, etc.
- Vagrantfile: Essential for defining the environment.
- Workflow:
- Scope: Identify OS, tools, and dependencies needed.
- Author: Write the
Vagrantfile. - Manage: Use Vagrant commands to control the environment.
- Share: Distribute the
Vagrantfileor packaged box for consistent setup.
vagrant init [box_name] [box_url]Initializes the environment. You can specify a box name and URL.
vagrant up- Fetches the box from the registry (if not local).
- Configures the provider.
- Applies the configuration from the Vagrantfile.
vagrant ssh- Sets up a secure SSH connection using an auto-generated key.
lsb_release -a- Verify you are inside the guest machine.
logout- Exit the guest machine.
vagrant suspend— Save and pause the machine state.vagrant resume— Resume the suspended machine.vagrant halt— Gracefully power off the machine (clean state on nextup).vagrant destroy— Remove the VM (does not delete the box).vagrant box remove <box_name>— Delete the box from your system.
You can automate software installation and configuration using provisioning scripts in your Vagrantfile (e.g., shell, Ansible, Puppet, etc.).
vagrant upThis command inializes the enviroment and runs all provision scripts If the machine already exists, the command will not run the provision
vagrant provisionRun provision scripts while the machine is running
vagrant up --provisionIf you want to force provision to be run on machine start up runs
Vagrant.configure("2") do |config|
# some code
config.vm.network "forwarded_port", guest: "8080", host: "8080"
# some code
endForwards the port from the guest machine to the host machine
Vagrant.configure("2") do |config|
config.vm.sync_folder "path_to_folder_on_host", "path_to_folder_on_guest", create: true
endSync the folder in host machine with the one on the guest machine (somewhat like the logic in docker of volumes)
_To simplify machine networking
libnss-mdnsandavahi-daemoncan be installed on each machine
# Destroy old machine, if it exists could cause conflicts
vagrant destory- Destroy the existing machine
- Create a script for installing common things that should exists in all VMs
# Services Configuration Reference
SERVICES = {
'backend' => {
ip: '192.168.56.11',
ports: { 8080 => 8080 }
},
'frontend' => {
ip: '192.168.56.12',
ports: { 8081 => 8081 }
}
}
Vagrant.configure("2") do |config|
# common configuration
config.vm.box = "hashicorp-education/ubuntu-24-04"
config.vm.box_version = "0.1.0"
# Common provisioning script for all VMs
config.vm.provision "shell", name: "common", path: "common-dependencies.sh"
config.vm.define "frontend" do |frontend|
frontend.vm.hostname = "frontend"
frontend.vm.network "private_network", ip: SERVICES['frontend'][:ip]
frontend.vm.network "forwarded_port", guest: 8080, host: 8080
# frontend.vm.synced_folder ...
frontend.vm.provision "shell", name: "start-frontend", inline: <<-SHELL
#... some code
# Get frontend IP dynamically (with 1 minute timeout)
for i in {1..30}; do
if BACKEND_IP=$(getent hosts backend.local | awk '{print $1}'); then
break
fi
echo "Waiting for backend.local to be resolvable..."
sleep 2
done
#... some code
docker run -d -p 8081:8081 \
--add-host backend.local:${BACKEND_IP} \
frontend
end
end
end
- Minimal ubuntu server
- port mapping for ssh
- passwordless sudo access
- apt update/upgrade/reboot
- install additional packages
- install guest additions
- customize files/config
- vagrant public key
- apt clean/autoremove
- truncate log files
- clean history
Note:
Before packaging, make sure your VM is powered off and the name matches exactly as shown in VirtualBox.
If you getVM not created. Moving on..., check the VM name and that it is managed by VirtualBox.**Vagrant needs the VM name
.vbox. The VM name is usually the folder name in~/VirtualBox VMs/and as listed in VirtualBox Manager.
For example, if you see~/VirtualBox VMs/ubuntu_24_server/ubuntu_24_server.vbox, your VM name isubuntu_24_server.
vagrant package --base ubuntu_24_server --output ubuntu_24_server.box ls ~/VirtualBox\ VMs/
# List files in the VM directory to verify VM name
ls ~/VirtualBox\ VMs/
# Add vagrant user to sudoers with passwordless sudo
echo 'vagrant ALL=(root) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/vagrant
# Update package lists
sudo apt update
# Upgrade installed packages
sudo apt upgrade
# Reboot
sudo reboot
# Install useful packages
sudo apt install -y build-essential dkms linux-headers-$(uname -r) curl wget git vim bash-completion nano tree
# Install VirtualBox Guest Additions (after mounting the CD)
sudo /mnt/VBoxLinuxAdditions.run
# Zeroing disk for better compression
sudo dd if=/dev/zero of=/empty bs=1M
sudo rm -f /empty
# Clean up apt cache and remove unnecessary packages
sudo apt autoremove -y
sudo apt clean
sudo rm -rf /var/lib/apt/lists/*
# Truncate all log files
sudo find /var/log -type f -exec truncate -s 0 {} \;
# Add vagrant public key for SSH access
curl -L https://raw.githubusercontent.com/hashicorp/vagrant/master/keys/vagrant.pub -o ~/.ssh/authorized_keys
# Clear bash history
history -c
history -wsudo mkdir -p /opt/vagrant/boxes
sudo mv 'your_gzip_box' /opt/vagrant/boxes
sudo chmod 644 /opt/vagrant/box/'your_gzip_box'
cd /opt/vagrant/box/
vagrant box add 'desired_name_for_box' 'your_gzip_box'
# vagrant unpacks the box in ~/.vagrant.d/boxesK3S is lightweight kubernetes, highly available certified distribution of kubernetes, can work in a constrainted enviromenent where ressources are critical. K3S is served as a less than 70Mb binary file.
Great For:
- IOT
- CI
- Embeded K8S
- Homelab
- Edge
- ...
K3S is fully compliant with k8s with following enhancement
- lightweight database based on sqlite3, etcd and other options are available
- Distributed as a single binary or minimal container image
- Packages the required dependencies for easy "batteries-included" cluster creation:
- containerd / cri-dockerd container runtime (CRI)
- Flannel Container Network Interface (CNI)
- CoreDNS Cluster DNS
- ...
- ...
- A Server Node is defined as a running host machine, running the command
k3s serverwith a database component and control-plane managed by K3S - A Agent Node is defined as a running host machine, runnning the command
k3s agentwitout any control-plane nor a database componenent - Both server and agent runs container runtime, kubelet, and CNI.
- Supervisor: Central orchestrator that manages and coordinates all server-side components and handles cluster initialization
- API Server: Kubernetes control plane component that exposes the Kubernetes API and serves as the frontend for the cluster
- Kube Proxy: Network proxy that maintains network rules and enables service discovery and load balancing across pods
- Scheduler: Assigns newly created pods to available nodes based on resource requirements and constraints
- Controller Manager: Runs controller processes that regulate the state of the cluster (node, replication, endpoints controllers)
- Kubelet: Node agent that communicates with the API server and manages pod lifecycle on the server node
- Flannel: Container Network Interface (CNI) plugin that provides overlay networking between pods across nodes
- Kine: Lightweight datastore interface that can use SQLite (embedded) or external databases (etcd, PostgreSQL, MySQL)
- containerd: Container runtime that manages the complete container lifecycle (pulling images, creating, starting, stopping containers)
- Tunnel Proxy: Establishes secure connection to the server and handles communication tunneling between agent and server
- Kube Proxy: Same as server - maintains network rules for service discovery and load balancing on the agent node
- Kubelet: Node agent that manages pod lifecycle on the agent node and reports node status to the control plane
- Flannel: CNI plugin that provides pod-to-pod networking and integrates with the cluster-wide network overlay
- containerd: Container runtime for managing containers on the agent node
- Pods: Smallest deployable units containing one or more containers, scheduled across both server and agent nodes
- Process: K3s runs as a single binary process on each node, simplifying deployment and management
- The server node can run with
embeded databaseorexternal databaseembeded databasewhen you have a single server node clusterexternal databasewhen kubertnetes control-plane availibility is critical, you have multiple server nodes. You can use etcd, PostgresSQL or MySQL
sequenceDiagram
participant Agent as K3s Agent Process
participant LB as Client-side Load Balancer
participant Server as K3s Server/Supervisor
participant API as Kube-APIServer
participant K8s as Kubernetes Secrets
Note over Agent, K8s: Initial Connection & Registration
Agent->>LB: Start websocket connection
LB->>Server: Connect via port 6443 (--server address)
Server->>LB: Accept connection
Note over Agent, API: Endpoint Discovery
Agent->>API: Retrieve kube-apiserver addresses
API->>Agent: Return service endpoint list (default namespace)
Agent->>LB: Add endpoints to load balancer
Note over LB, Server: Stable Connections
LB->>Server: Maintain connections to all servers
Note right of LB: Tolerates individual server outages
Note over Agent, K8s: Authentication & Password Management
Agent->>Agent: Generate random node password
Agent->>Agent: Store at /etc/rancher/node/password
Agent->>Server: Register with node cluster secret + password
Server->>K8s: Store password as Kubernetes secret
Note right of K8s: Stored in kube-system namespace<br/>Template: <host>.node-password.k3s
Note over Agent, K8s: Re-registration Scenarios
rect rgb(255, 245, 238)
Note over Agent, K8s: Node Cleanup Required
Agent->>Server: Remove /etc/rancher/node directory
Server->>K8s: Delete old node entry & password secret
Agent->>Server: Allow node to rejoin cluster
end
rect rgb(238, 255, 238)
Note over Agent, K8s: Unique Node ID Option
Agent->>Agent: Launch with --with-node-id flag
Agent->>Agent: Append unique ID to hostname
Agent->>Agent: Store node ID in /etc/rancher/node/
end
- Websocket Connection: Initiated by k3s agent process
- Client-side Load Balancer: Maintains endpoint list and stable connections
- Port 6443: Default connection port to supervisor and kube-apiserver
- Password Storage:
/etc/rancher/node/password(agent) andkube-systemnamespace (server) - Secret Template:
<host>.node-password.k3sformat for node passwords
- Node Cleanup: Remove
/etc/rancher/nodedirectory and delete cluster node entry for re-registration - Unique Node IDs: Use
--with-node-idflag for frequent hostname reuse scenarios - High Availability: Load balancer tolerates individual server outages
K3S can be installed using official install script
- K3S can be install as a service on systemd or open rc based system
- Download K3S binary and run it manually
# server node
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--node-ip=<ip> --advertise-ip=<ip>" sh -
# Agent node | `K3S_URL` causes the installer to install K3S as an agent
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--node-ip=<ip>" K3S_URL=https://myserver:6443 K3S_TOKEN=mynodetoken sh --
--advertise-address value(listener) IPv4/IPv6 address that apiserver uses to advertise to members of the cluster (default: node-external-ip/node-ip) -
--node-ip value, -i value(agent/networking) IPv4/IPv6 addresses to advertise for node -
Installing only the server node is considered a fully functional kubernetes cluster, with Database, control-plane, Kublet, container runtime and is ready to host a workload of pods.
-
Configuring K3S to restart if node crashes or restart
-
Installing Kubectl, crictl, ... and additional utilies
-
Kubeconfig is written at
/etc/rancher/k3s/k3s.yaml, and kubectl installed by K3S will use it. -
Agent will register to the server listenning on the given address
-
K3S_TOKENcan be found at/var/lib/rancher/k3s/server/node-tokenon your server node -
If some machines have same name, provide K3S_NODE_NAME for each node with a unique name
Note: You may need to change permission of
/etc/rancher/k3s/k3s.yamlchmod 644 /etc/rancher/k3s/k3s.yaml
This section covers the complete process of setting up a K3S cluster with a server node and worker (agent) node using Vagrant.
- Server Node (mfouadiS): Control plane + database + worker capabilities
- Worker Node (mfouadiSW): Agent node that joins the cluster
- Network: Private network (192.168.56.0/24) for inter-node communication
- Vagrant Box: Custom Ubuntu 24.04 server box (
ubuntu_24_server) - VirtualBox: As the provider
- Environment Configuration:
.envfile with cluster settings
Create a .env file in the project root with the cluster configuration:
# .env file
K3S_SERVER_TOKEN=<actual_token_from_server>
SERVER_NODE_IP=192.168.56.110
SERVER_AGENT_NODE_IP=192.168.56.111
K3S_SERVER_URL=https://192.168.56.110:6443Important: The
K3S_SERVER_TOKENmust be obtained from the server node after installation.
Configure the multi-machine environment with proper networking and provisioning:
# Load environment variables
def load_env_file(file_path)
if File.exist?(file_path)
File.readlines(file_path).each do |line|
line = line.strip
next if line.empty? || line.start_with?('#')
key, value = line.split('=', 2)
ENV[key] = value if key && value
end
end
end
load_env_file('../.env')
SERVICES = {
"mfouadiS" => { ip: ENV['SERVER_NODE_IP'] },
'mfouadiSW' => { ip: ENV['SERVER_AGENT_NODE_IP'] }
}
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu_24_server"
# Server Node Configuration
config.vm.define "mfouadiS" do |server|
server.vm.hostname = 'mfouadiS'
server.vm.network 'private_network', ip: SERVICES['mfouadiS'][:ip]
server.vm.provision "shell", path: "scripts/common-dependencies.sh"
server.vm.provision "shell",
path: "scripts/install-k3s-server.sh",
env: {"SERVER_NODE_IP" => ENV['SERVER_NODE_IP']}
end
# Worker Node Configuration
config.vm.define "mfouadiSW" do |worker|
worker.vm.hostname = 'mfouadiSW'
worker.vm.network "private_network", ip: SERVICES['mfouadiSW'][:ip]
worker.vm.provision "shell", path: "scripts/common-dependencies.sh"
worker.vm.provision "shell",
path: "scripts/install-k3s-agent.sh",
env: {
"K3S_SERVER_TOKEN" => ENV['K3S_SERVER_TOKEN'],
"SERVER_AGENT_NODE_IP" => ENV['SERVER_AGENT_NODE_IP'],
"K3S_SERVER_URL" => ENV['K3S_SERVER_URL']
}
end
end#!/bin/bash
sudo apt update
sudo apt-get install -y iputils-ping#!/bin/bash
# Install K3S server with specific node IP
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--node-ip=$SERVER_NODE_IP --advertise-address=$SERVER_NODE_IP" sh -
# Make kubeconfig accessible
sudo chmod 644 /etc/rancher/k3s/k3s.yaml#!/bin/bash
# Install K3S agent and join cluster
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--node-ip=$SERVER_AGENT_NODE_IP" K3S_URL=$K3S_SERVER_URL K3S_TOKEN=$K3S_SERVER_TOKEN sh -
# Wait for agent to start
sleep 5
# Configure kubectl access for vagrant user
sudo mkdir -p /home/vagrant/.kube
# Copy server's kubeconfig and modify for remote access
sudo tee /home/vagrant/.kube/config > /dev/null <<EOF
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: <CA_DATA_FROM_SERVER>
server: ${K3S_SERVER_URL}
name: default
contexts:
- context:
cluster: default
user: default
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
user:
client-certificate-data: <CLIENT_CERT_FROM_SERVER>
client-key-data: <CLIENT_KEY_FROM_SERVER>
EOF# Start the server node first
vagrant up mfouadiS --provision
# Get the server token (needed for agent connection)
vagrant ssh mfouadiS -c "sudo cat /var/lib/rancher/k3s/server/node-token"Update your .env file with the actual token from step 1:
K3S_SERVER_TOKEN=K10<hash>::server:<hash># Start the worker node
vagrant up mfouadiSW --provision# Check cluster status from server
vagrant ssh mfouadiS -c "kubectl get nodes"
# Check from worker (should also work)
vagrant ssh mfouadiSW -c "kubectl get nodes"Error: token CA hash does not match the Cluster CA certificate hash
Solution: Update the K3S_SERVER_TOKEN in .env with the current token from the server.
Error: the server has asked for the client to provide credentials
Solution: Ensure kubeconfig is properly copied with correct certificates.
# Test connectivity between nodes
vagrant ssh mfouadiSW -c "ping 192.168.56.110"
vagrant ssh mfouadiSW -c "curl -k https://192.168.56.110:6443"# Check service status
vagrant ssh mfouadiSW -c "sudo systemctl status k3s-agent"
vagrant ssh mfouadiS -c "sudo systemctl status k3s"
# Check logs
vagrant ssh mfouadiSW -c "sudo journalctl -u k3s-agent -f"If kubectl authentication fails, manually set up kubeconfig:
# On worker node
vagrant ssh mfouadiSW
# Copy server's kubeconfig and modify server URL
sudo mkdir -p /home/vagrant/.kube
sudo tee /home/vagrant/.kube/config > /dev/null <<EOF
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: <SERVER_CA_DATA>
server: https://192.168.56.110:6443
name: default
contexts:
- context:
cluster: default
user: default
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
user:
client-certificate-data: <CLIENT_CERT_DATA>
client-key-data: <CLIENT_KEY_DATA>
EOF
sudo chown vagrant:vagrant /home/vagrant/.kube/config
sudo chmod 600 /home/vagrant/.kube/config# Check cluster nodes
kubectl get nodes -o wide
# Check node status
kubectl describe nodes
# Check system pods
kubectl get pods -A
# Test pod deployment
kubectl run test-pod --image=nginx --port=80
kubectl get pods
kubectl delete pod test-pod- Boot Order: Always start the server node before worker nodes
- Token Management: Server token changes when server is recreated
- Network Requirements: Both nodes must be on the same network segment
- Certificate Authority: Worker nodes validate server certificates
- User Access: The
vagrantuser has kubectl access - SSH Access: Use
vagrant ssh <machine_name>to access the VMs
- Production: Use proper TLS certificates instead of
insecure-skip-tls-verify - Token Storage: Store tokens securely, not in version control
- Network: Use firewalls and network policies in production
- Access Control: Implement RBAC for user permissions
Environment:
- Ubuntu
- VirtualBox
- Secure Boot enabled in BIOS
During VirtualBox installation, a Machine Owner Key (MOK) is created. Set a password when prompted. On reboot, enter this password to sign kernel modules, allowing VirtualBox access.
To disable Kernel-based Virtual Machine (KVM) and give VirtualBox exclusive access to hardware virtualization:
sudo modprobe -r kvm_intel kvmNote: KVM may reload after reboot.
By default, kubectl looks for a file named config in the $HOME/.kube directory. You can specify other kubeconfig files by setting the KUBECONFIG environment variable or by setting the --kubeconfig flag.
- The common format of a kubectl command is:
kubectl action resource. You can add --help.
1 - kubectl create deployment provide the deployment name and app image location. --image=.
2 - kubectl get deployments
3 - kubectl proxy is mainly for accessing the Kubernetes API, not for exposing your application to the public internet. For that, you'll typically use Services of type NodePort, LoadBalancer, or Ingress. "kubectl proxy" does not expose Pods directly; it exposes the Kubernetes API server locally, allowing you to access cluster resources (including Pods) via the API from your local machine.
Note: if your running cluster in a VM: forward port you app from guest to host add
--address=0.0.0.0and--accept-hosts='.*'options tokubectl proxy. So, Kubectl doesn't listen only locally inside guest machine
4 - curl your app
5 - Accessing Pod, based on POD name
# First we need to get the Pod name, and we'll store it in the environment variable POD_NAME.
export POD_NAME=$(kubectl get pods -o go-template --template '{{range .items}}{{.metadata.name}}{{"\n"}}{{end}}')
echo Name of the Pod: $POD_NAME
# You can access the Pod through the proxied API, by running:
curl http://localhost:8001/api/v1/namespaces/default/pods/$POD_NAME:8080/proxy/
Note: The API Server automatically creates an endpoint for each pods, based on pod' s name. That is also accessible through the proxy
https://www.youtube.com/watch?v=ePyFJ7Hd57Q&t=23s&ab_channel=GOTOConferences
- https://fluxcd.io/ : Automating GitOps / Deployment
- https://github.com/bitnami-labs/sealed-secrets : Managing Secrets
- Creating a Base Box
- Vagrant Tutorial Video by The Urban Penguin
- Vagrant Tutorials
- Vagrant Ansible Provisioning
- Kubernetes Concepts Overview
- Kubernetes Components
- Kubernetes Architecture
- K3s Documentation
- K3s Architecture
- K3s Quick Start Install Script
- K3s Server CLI Reference
- https://unix.stackexchange.com/questions/134483/why-is-my-ethernet-interface-called-enp0s10-instead-of-eth0
- https://www.ruby-lang.org/en/documentation/quickstart/
