Provision and operate a Kubernetes cluster with the convenience of a CSP-managed Kubernetes service. Designed for homelabs, on-premises, and edge environments.
- Deploys k3s, a single-binary Kubernetes distribution
- Employs k3s' air-gapped installation method
- Implements a virtual IP address with automatic failover for the Kubernetes API server (powered by Keepalived)
- Delivers a highly available control plane with load-balanced server nodes (powered by HAProxy)
- Includes built-in SELinux support using the SELinux policy package
- Offers curated configuration options including node taints/labels, cluster networking, and Kubernetes component flags
- Integrates with k3s’ auto-deploying manifests and registry configuration features
- Provides recommendations for CIS hardening and graceful node shutdown, delivered via optional Terraform submodules
- Performs in-place upgrades with coordinated node draining and optional system upgrades
- Manages day-2 node additions and removals
- Facilitates Kubernetes certificate rotation
- Enables seamless Terraform-native cluster access, no manual steps required
- Cloud-agnostic by design -- provisions clusters without dependencies on cloud services (e.g. Load Balancers, PKI, KMS)
- Day-2 friendly operations -- avoids reliance on machine bootstrapping tools (e.g. Cloud-init, Ignition), simplifying troubleshooting and management at scale
- Self-contained architecture -- eliminates the need for an external Ansible control plane (e.g. AWX/Ansible Tower)
- Open source -- no paid tiers, proprietary unlocks, or per-node fees
- Terraform installed (version 1.12 or later)
ansible-navigatorinstalled (version 25.4.0 or later)- Container engine (
podmanordocker) with support foramd64orarm64images (Ansible execution environment) - Access to
github.comto download k3s release artifact tarballs
- Fedora CoreOS 43 or later (
amd64,arm, orarm64architecture) - Configured with a non-root user with passwordless
sudoaccess - Reachable via SSH from the Terraform execution host/runner
- Network connectivity between all machines
- Python 3 installed on the host (required by Ansible)
- Access to
quay.ioand the Fedora repositories (via direct internet access, a configured proxy, or local mirror/container registry) for system upgrades and package downloads. Note: Using Zincati for automatic updates requires additional access
Additional examples available.
module "k3s" {
source = "marshallford/k3s/ansible"
ssh_private_keys = [
{
name = "example"
data = var.private_key
}
]
api_server = {
virtual_ip = "192.168.1.99"
virtual_router_id = 1
}
tokens = {
server = "some-token"
agent = "some-token"
}
server_machines = { for name, addr in local.server_machines : name => {
name = name
ssh = {
address = addr
}
config = {
cluster_init = name == "a",
}
} }
agent_machine_groups = {
"example" = { for name, addr in local.agent_machines : name => {
name = name
ssh = {
address = addr
}
} }
}
}
provider "kubernetes" {
host = module.k3s.server
cluster_ca_certificate = module.k3s.ephemeral_credentials.cluster_ca_certificate
client_certificate = module.k3s.ephemeral_credentials.client_certificate
client_key = module.k3s.ephemeral_credentials.client_key
}- State file sensitivity -- if
kubeconfig_block_typeis set todataorresource, the cluster admin kubeconfig will be persisted in both Terraform’s state file and any generated plan files. These files should be treated as sensitive and protected accordingly. - Certificate management -- Kubernetes certificates are rotated as needed on module apply, but it is the operator’s responsibility to re-run Terraform before they expire. See the k3s certificate docs for details.
- CIS Kubernetes benchmark -- many controls are satisfied by default in k3s or via the provided
cis-hardensubmodule. Controls outside this scope remain the operator’s responsibility. - Machine updates -- use Zincati for automatic updates or configure
system_upgrade_triggerfor coordinated updates and reboots through this module. - Access control -- exposing machines or the Kubernetes API server directly to the internet increases risk. Access should be restricted through VPN, bastion, or firewall controls.
- Only tested with
amd64machines on an IPv4 network - SELinux package cannot be upgraded (upstream issue)
- Removal of the
cluster-initserver node is untested - Machines removed must be fully wiped or have their disks reprovisioned prior to rejoining the cluster
- Terraform-managed machine resources must set lifecycle
create_before_destroy = trueto ensure correct ordering during node removal
- Ansible roles equivalent to Butane snippets
- Support for custom Ansible plays (pre, post, etc)
- Butane snippet for NTP
- Firewall rules
- Keepalived
max_auto_priorityoption - Assert podman version
- k3s token rotation
- Stop Zincati at start of playbook and start at the end
- Knownhosts management
