Technical Documentation: k0sproject/k0s
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
k0s - The Zero Friction Kubernetes
Overview
k0s is an open source, all-inclusive Kubernetes distribution, which is configured with all of the features needed to build a Kubernetes cluster and packaged as a single binary for ease of use. Due to its simple design, flexible deployment options and modest system requirements, k0s is well suited for
- Any cloud
- Bare metal
- Edge and IoT
k0s drastically reduces the complexity of installing and running a CNCF certified Kubernetes distribution. With k0s new clusters can be bootstrapped in minutes and developer friction is reduced to zero. This allows anyone with no special skills or expertise in Kubernetes to easily get started.
k0s is distributed as a single binary with zero host OS dependencies besides the host OS kernel. It works with any Linux without additional software packages or configuration. Any security vulnerabilities or performance issues can be fixed directly in the k0s distribution that makes it extremely straightforward to keep the clusters up-to-date and secure.
Key Features
- Certified and 100% upstream Kubernetes
- Multiple installation methods: single-node, multi-node, airgap and Docker
- Automatic lifecycle management with k0sctl: upgrade, backup and restore
- Modest system requirements (1 vCPU, 1 GB RAM)
- Available as a single binary with no external runtime dependencies besides the kernel
- Flexible deployment options with control plane isolation as default
- Scalable from a single node to large, high-available clusters
- Supports custom Container Network Interface (CNI) plugins (Kube-Router is the default, Calico is offered as a preconfigured alternative)
- Supports custom Container Runtime Interface (CRI) plugins (containerd is the default)
- Supports all Kubernetes storage options with Container Storage Interface (CSI)
- Supports a variety of datastore backends: etcd (default for multi-node clusters), SQLite (default for single node clusters), MySQL, and PostgreSQL
- Supports x86-64, ARM64, ARMv7 and RISC-V
- Includes Konnectivity service, CoreDNS and Metrics Server
Getting Started
If you'd like to try k0s, please jump in to our:
- Quick Start Guide - Create a full Kubernetes cluster with a single node that includes both the controller and the worker.
- Install using k0sctl - Deploy and upgrade multi-node clusters with one command.
- NanoDemo - Watch a .gif recording on how to create a k0s instance.
- Run k0s in Docker - Run k0s controllers and workers in containers.
- For docs, tutorials, and other k0s resources, see docs main page.
Join the Community
The main community communication channel is #k0s-users on Kubernetes Slack.
There you'll find like-minded people and reach out for assistance and
discussions.
We welcome your help in building k0s! If you are interested, we invite you to
join the #k0s-dev channel on Kubernetes Slack and check out the contributing
guidelines.
We also run a community blog that's open to posts from anyone. Subscribe to
its RSS feed to stay up to date.
Community calls
We hold regular community calls. Everyone is welcome to drop by to ask
questions, discuss ideas or planned contributions, or just chat:
- Community office hours, Eastern Hemisphere (EMEA-friendly time)
- Community office hours, Western Hemisphere (Americas-friendly time)
- Maintainers call, open to everyone but meant for maintainers to sync up on
plans and work
All events are listed in the k0sproject meeting calendar, and the
meeting minutes are public.
Adopters
k0s is used across diverse environments, from small-scale far-edge deployments
to large data centers. Share your use case and add yourself to the list of
adopters.
Motivation
We have seen a gap between the host OS and Kubernetes that runs on top of it: How to ensure they work together as they are upgraded independent from each other? Who is responsible for vulnerabilities or performance issues originating from the host OS that affect the K8S on top?
→ k0s is fully self contained. It’s distributed as a single binary with no host OS deps besides the kernel. Any vulnerability or perf issues may be fixed in k0s Kubernetes.
We have seen Kubernetes with cumbersome lifecycle management, high minimum system requirements, weird host OS and infra restrictions, and/or need to use different distros to meet different use cases.
→ k0s is designed to be lightweight at its core. It comes with a tool to automate cluster lifecycle management. It works on any host OS and infrastructure, and may be extended to work with any use cases such as edge, IoT, telco, public clouds, private data centers, and hybrid & hyper converged cloud applications without sacrificing the pure Kubernetes compliance or amazing developer experience.
Status
k0s is ready for production (starting from v1.21.0+k0s.0). Since the initial release of k0s back in November 2020, we have made numerous releases, improved stability, added new features, and most importantly, listened to our users and community in an effort to create the most modern Kubernetes product out there. The active development continues to make k0s even better.
Scope
While some Kubernetes distros package everything and the kitchen sink, k0s tries to minimize the amount of "add-ons" to bundle in. Instead, we aim to provide a robust and versatile "base" for running Kubernetes in various setups. Of course we will provide some ways to easily control and setup various "add-ons", but we will not bundle many of those into k0s itself. There are a couple of reasons why we think this is the correct way:
- Many of the addons such as ingresses, service meshes, storage etc. are VERY opinionated. We try to build this base with fewer opinions. :D
- Keeping up with the upstream releases with many external addons is very maintenance heavy. Shipping with old versions does not make much sense either.
With strong enough arguments we might take in new addons, but in general those should be something that are essential for the "core" of k0s.
Build
The requirements for building k0s from source are as follows:
- GNU Make (v3.81 or newer)
- A POSIX shell
- coreutils
- findutils
- Docker
All of the compilation steps are performed inside Docker containers, no
installation of Go is required.
The k0s binary can be built in different ways:
The "k0s" way, self-contained, all binaries compiled from source, statically
linked, including embedded binaries:
makeThe "package maintainer" way, without building and embedding the required
binaries. This assumes necessary binaries are provided separately at runtime:
make EMBEDDED_BINS_BUILDMODE=noneDocker build integration is enabled by default. However, in environments without
Docker, you can use the Go toolchain installed on the host system to build k0s
without embedding binaries. Note that static linking is not possible with
glibc-based toolchains:
make DOCKER='' EMBEDDED_BINS_BUILDMODE=none BUILD_GO_LDFLAGS_EXTRA=''Note that the k0s build system does not currently support building the embedded
binaries without Docker. However, the embedded binaries can be built
independently using Docker:
make -C embedded-binsBuilds can be done in parallel:
make -j$(nproc)Smoke test
Additionally to the requirements for building k0s, the smoke tests do require
a local Go installation. you can run ./vars.sh go_version in a terminal to
find out the version that's being used to build k0s. It will print the
corresponding Go version to stdout.
To run a basic smoke test after build:
make check-basicOverview (`docs/README.md`)
k0s - The Zero Friction Kubernetes
{%
include-markdown "../README.md"
start=""
end=""
%}
{%
include-markdown "../README.md"
start=""
end=""
%}
Getting Started
- Quick Start Guide for creating a full Kubernetes cluster with a single node.
- Install using k0sctl - Deploy and upgrade multi-node clusters with one command.
- Run k0s in Docker - Run k0s controllers and workers in containers.
Demo
{%
include-markdown "../README.md"
start=""
end=""
%}
Adopters
k0s is used across diverse environments, from small-scale far-edge deployments
to large data centers. Share your use case and add yourself to the list of
adopters.
Commercial Support
Mirantis offers technical support, professional services and training for k0s. The support subscriptions include, for example, prioritized support (Phone, Web, Email) and access to verified extensions on top of your k0s cluster.
For any k0s inquiries, please contact us via email [email protected].
{%
include-markdown "../README.md"
start=""
end=""
%}
{%
include-markdown "../README.md"
start=""
end=""
%}
Overview (`inttest/README.md`)
Integration tests a.k.a e2e testing
This folder is the root of k0s integration tests. These tests are such that run the actual k0s clusters, currently using bootloose as the target environment.
Running the tests
There's a Makefile defining the tests as a set of make targets.
Test design
We're currently building the tests as Golang tests with the help of testify test suite concept. The suite concept allows us to have suite level setup and teardown functionality so we can bootstrap and delete the test environment properly during testing. The suite setup phase creates the "infrastructure" for the tests and the teardown, as the name implies, deletes the infra.
Keeping the test env after tests
Sometimes, especially when debugging some test failures, it's good to leave the environment running after the tests have ran. To control that behavior there's an env variable called K0S_KEEP_AFTER_TESTS. The value given to that has the following logic:
- no value or
K0S_KEEP_AFTER_TESTS="never": The test env is NOT left running regardless of the test results K0S_KEEP_AFTER_TESTS="always": The test env is left running regardless of the test resultsK0S_KEEP_AFTER_TESTS="failure": The test env is left running only if the tests have failed
The test output show how to run manual cleanup for the environment, something like:
TestNetworkSuite: bootloosesuite.go:138: bootloose cluster left intact for debugging. Needs to be manually cleaned with: bootloose delete --config /tmp/afghzzvp-bootloose.yamlThis allows you to run manual cleanup after you've done the needed debugging.
Which tests to run when?
The plan is to run only the basic (and quick) smoke tests on each PR commit. We should build some bot like functionality to run longer and more expensive tests using some trigger before the final merge of a PR. Naturally we should be able to run any of the tests locally, or at least triggered locally, to ensure we can actually debug what is happening.
Overview (`hack/ostests/README.md`)
OpenTofu modules for k0s OS testing
Provisioning of k0s test clusters using different operating system stacks and
network providers based on OpenTofu and k0sctl.
By default, a test cluster will have the following properties:
- 3 controller nodes
- 2 worker nodes
- podCIDR is set to 10.244.0.0/16
- Node-local load balancing is enabled.
- All the rest of the configuration is left at its defaults.
Two workers are a minimum requirement for the Kubernetes conformance tests.
Requirements
- OpenTofu >= 1.8
For the local plumbing:
For the AWS infra:
- Have the CLI credentials setup, in the usual AWS CLI way.
- Have a configured default region. That region will be targeted by OpenTofu.
Deploying a cluster
Be sure to meet the requisites listed above. Configure AWS credentials and
region. This is just an example, there are other ways to do it.
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_SESSION_TOKEN=...
export AWS_REGION=...Select the desired cluster configuration for OpenTofu. This is just one example;
other methods of passing configuration options are covered in the official
OpenTofu documentation.
export TF_VAR_os=alpine_3_24
export TF_VAR_arch=x86_64
export TF_VAR_k0s_version=stable
export TF_VAR_k0s_network_provider=calico
export TF_VAR_k0s_kube_proxy_mode=ipvsApply the configuration:
tofu applyOpenTofu configuration variables
`os`: Operating system stack
al2023: Amazon Linux 2023alpine_3_21: Alpine Linux 3.21alpine_3_24: Alpine Linux 3.24centos_9: CentOS Stream 9centos_10: CentOS Stream 10 (Coughlan)debian_11: Debian GNU/Linux 11 (bullseye) (supported until 2026-08-31)debian_12: Debian GNU/Linux 12 (bookworm) (supported until 2028-06-30)fcos_stable: Fedora CoreOS stable streamfedora_41: Fedora Linux 41 (Cloud Edition)flatcar: Flatcar Container Linux by Kinvolkoracle_8_9: Oracle Linux Server 8.9oracle_9_3: Oracle Linux Server 9.3rhel_7: Red Hat Enterprise Linux Server 7.9 (Maipo)rhel_8: Red Hat Enterprise Linux 8.10 (Ootpa)rhel_9: Red Hat Enterprise Linux 9.7 (Plow)rocky_8: Rocky Linux 8.10 (Green Obsidian)rocky_9: Rocky Linux 9.5 (Blue Onyx)sles_15: SUSE Linux Enterprise Server 15 SP6ubuntu_2004: Ubuntu 20.04 LTSubuntu_2204: Ubuntu 22.04 LTSubuntu_2404: Ubuntu 24.04windows_server_2022: Windows Server 2022 (runs the control plane on Alpine 3.23)
`arch`: Node architecture
The underlying processor architecture for the to-be-provisioned cluster. Note
that not all operating systems support all architectures.
x86_64arm64
`k0sctl_skip`: Skip k0s provisioning altogether
Just provision the infrastructure, but no k0s cluster. This may be used for
development and testing purposes.
Assuming the AWS credentials are available, it can be used like this:
tofu init
export TF_VAR_os=alpine_3_24
export TF_VAR_k0sctl_skip=true
tofu apply
tofu output -json | jq -r '
(.ssh_private_key_filename.value | @sh) as $keyFile
| .hosts.value[]
| select(.connection.type = "ssh")
| . as {name: $host, ipv4: $ip, connection: {username: $user}}
| "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i \($keyFile) \($user)@\($ip) # \($host)"
'`k0s_version`: The k0s version to deploy
This may be a fixed version number, "stable" or "latest".
`k0s_executable_path`: The k0s version to deploy
Path to the k0s executable to use, or null if it should be downloaded. Note that
for Windows, the .exe suffix is appended automatically.
`k0s_network_provider`: Network provider
kuberoutercalico
`k0s_kube_proxy_mode`: Mode of operation for kube-proxy
iptablesipvs
See Kubernetes's IPVS README for details.
Adding a new operating system
- Navigate to modules/os/ and add a new file
os_.tf.
Have a look at the otheros_*.tffiles for how it should look like. - Add a new OS entry to modules/os/main.tf.
- Update this README.
- Test it: Be sure to have the requisites ready, as described at the top of this
README, then dotofu apply -var=os=. When done, don't
forget to clean up:tofu destroy -var=os=. - Update the nightly trigger and matrix workflow with the new OS ID.
Updating the existing OS versions
New OS version usually mean just updating the OS AMI filtering criteria.
To test the AMI filtering locally, you can do the following:
tofu apply -refresh-only -target=module.os.data.aws_ami.<the-os-id>
tofu state show "module.os.data.aws_ami.<the-os-id>[0]"GitHub Actions workflows
There's a reusable GitHub Actions workflow available in ostests-e2e.yaml. It
will deploy the OpenTofu resources and perform Kubernetes conformance tests
against the provisioned test cluster.
Launch a workflow run
There's a nightly trigger for the OS testing workflow. It will select and run
a single testing parameter combination each day. There's also a matrix
workflow that exposes more knobs and can be triggered manually, e.g. via gh:
$ gh workflow run ostests-matrix.yaml --ref some/experimental/branch \
-f oses='["alpine_3_24"]' \
-f network-providers='["calico"]'
✓ Created workflow_dispatch event for ostests-matrix.yaml at some/experimental/branch
To see runs for this workflow, try: gh run list --workflow=ostests-matrix.yamlAdd -f fail-fast=true to the above command line to stop the e2e test suite
after the first test failure has been recorded.
TODO
- Figure out the best/canonical way to change host names of the AWS instances
Architecture Index (`docs/architecture/index.md`)
Architecture
Note: As k0s is a dynamic project, the product architecture may occasionally outpace the documentation. The high level concepts and patterns, however, should always apply.
Packaging
The k0s package is a single, self-extracting binary that embeds Kubernetes binaries, the benefits of which include:
- Statically compiled
- No OS-level dependencies
- Requires no RPMs, dependencies, snaps, or any other OS-specific packaging
- Provides a single package for all operating systems
- Allows full version control for each dependency
Control plane
As a single binary, k0s acts as the process supervisor for all other control plane components. As such, there is no container engine or kubelet running on controllers by default, which thus means that a cluster user cannot schedule workloads onto controller nodes.
Using k0s you can create, manage, and configure each of the components, running each as a "naked" process. Thus, there is no container engine running on the controller node.
Storage
Kubernetes control plane typically supports only etcd as the datastore. k0s, however, supports many other datastore options in addition to etcd, which it achieves by including kine. Kine allows the use of a wide variety of backend data stores, such as MySQL, PostgreSQL, SQLite, and dqlite (refer to the spec.storage documentation).
In the case of k0s managed etcd, k0s manages the full lifecycle of the etcd cluster. For example, by joining a new controller node with k0s controller "long-join-token" k0s automatically adjusts the etcd cluster membership info to allow the new member to join the cluster.
Note: k0s cannot shrink the etcd cluster. As such, to shut down the k0s controller on a node that node must first be manually removed from the etcd cluster.
Worker node
As with the control plane, with k0s you can create and manage the core worker components as naked processes on the worker node.
By default, k0s workers use containerd as a high-level runtime and runc as a low-level runtime. Custom runtimes are also supported, refer to Using custom CRI runtimes.
Contributors Index (`docs/contributors/index.md`)
Contributing to k0s
Thank you for taking the time to contribute to k0s! This page gives you an
overview of how to contribute, and where to find the details.
Please follow the k0sproject community's code of conduct and
the k0sproject AI policy in all your interactions with the project.
If you have found a security vulnerability, don't disclose it publicly. Report
it confidentially, as described in the security policy.
Contributing to the repository
K0s is more than its code. Contributions to the documentation are as welcome and
as valuable as contributions to the code base.
Seek early feedback
If there's more than one way to approach your change, reach out via GitHub
issues or other community channels before
you start building it. Alternatives to a feature and different implementation
paths are easier to compare in the abstract than in a finished diff. Discussing
early also shows whether a change fits the project's direction, and whether
someone is already working on it.
Code
The code guidelines collect the conventions of the k0s
code base beyond what the linters enforce, such as comments, error handling,
logging, and how to maintain compatibility across versions.
If your change affects how k0s is used or configured, update the documentation
along with it.
The testing page explains how to run the linters, unit tests, and
integration tests locally, how to debug CI failures, and how to run CI on your
own fork.
Documentation
The documentation lives under the docs directory and is published to
https://docs.k0sproject.io. Fixing a typo, clarifying a confusing section, or
writing a guide for something that isn't covered yet are all great
contributions.
The documentation page explains how the docs are built, how to add
a page to the navigation, and how to preview your changes locally.
Pull requests
All changes, to the documentation and to the code alike, arrive as pull
requests. The GitHub workflow page walks you through the
process of
- forking the repository,
- writing commits that serve one purpose each and are signed off to certify
the Developer Certificate of Origin, - opening the pull request,
- and reworking your commits after a review round.
Before opening a pull request, run the linters and related tests locally, as
described on the testing page. CI runs on a pull request only
after a maintainer has approved it, so catching problems locally saves a round
trip.
Contributing beyond the repository
Not every contribution is a change to this repository. There are many ways to
help k0s and the k0sproject community as a whole:
- Report bugs via GitHub issues.
A clear report with steps to reproduce, the k0s version, and relevant logs is
a valuable contribution on its own. - Help others in GitHub issues and on #k0s-users.
Answering a question, sharing how you solved a problem, or adding what you
know to an issue someone else reported helps the community. - Help the maintainers by testing and reviewing pull requests.
Trying out a change in your environment, or reading it with a fresh pair of
eyes, is valuable feedback that speeds up the review. - Share your experience.
Write a blog post, give a talk, or tell us about your use case.
Add yourself to the list of adopters, and let us know on Slack, so we can
spread the word. - Join the community calls.
Discuss ideas, ask questions, or just get to know the people behind k0s.
License
By contributing, you agree that your contributions will be licensed as follows:
- All content residing under the "docs/" directory of this repository is licensed under "Creative Commons Attribution Share Alike 4.0 International" (CC-BY-SA-4.0). See docs/LICENSE for details.
- Content outside of the above mentioned directories or restrictions above is available under the "Apache License 2.0".
Airgap Install (`docs/airgap-install.md`)
Air gapped Installation
You can install k0s in environments without Internet access. Air gapped
installations require an image bundle that contains all the container images
that would normally be pulled over the network. K0s uses so-called OCI archives
for this: Tarball representations of an OCI Image Layout. They allow for
multiple images to be packed into a single file. K0s will watch for image
bundles in the <data-dir>/images folder will automatically import them into
the container runtime.
There are several ways to obtain an image bundle:
- Use the pre-built image bundles for different target platforms that are
created for each k0s release. They contain all the images for the default k0s
image configuration and can be downloaded from
the [GitHub releases page]. - Create your own image bundle. In this case, you can easily customize the
bundle to include container images that are not used by default in k0s.
Note: When importing image bundles, k0s uses "loose" platform
matching. For
example, on arm/v8, k0s will also import arm/v7, arm/v6, and arm/v5 images. This
means that your bundle can contain multi-arch images, and the import will be
done using platform compatibility.
[GitHub releases page]: https://github.com/k0sproject/k0s/releases/{{{ k0s_version }}}
Creating image bundles
Using k0s built-in tooling
k0s ships with the k0s airgap sub-command, which is
dedicated for tooling for airgapped environments. It allows for listing the
required images for a given configuration, as well as bundling them into an OCI
Image Layout archive.
Create the list of images required by k0s.
shellk0s airgap list-images --all >airgap-images.txtReview this list and edit it according to your needs.
Create the image bundle.
shellk0s airgap bundle-artifacts -v -o image-bundle.tar <airgap-images.txt
From a running worker node
As containerd pulls all the images during the k0s worker normal bootstrap, you
can use it to build the OCI bundle with images.
Use the following commands on a machine with an installed k0s worker:
k0s ctr images export image-bundle.tar $(k0s airgap list-images | xargs)Using third-party tools
There are several CLI tools that can help you fetch OCI artifacts and manage OCI
Image Layouts, such as skopeo, oras, or crane. The following is an example
uses Docker:
Docker
Create the list of images required by k0s.
shellk0s airgap list-images --all >airgap-images.txtReview this list and edit it according to your needs.
Pull the images.
shellxargs -I{} docker pull {} <airgap-images.txtCreate the bundle.
shelldocker image save -o image-bundle.tar $(xargs <airgap-images.txt)
Placing image bundles on worker nodes
By hand
Copy the image-bundle.tar to the target machine into the images directory in
the k0s data directory. Copy the bundle only to the worker nodes. Controller
nodes don't use it.
# mkdir -p /var/lib/k0s/images
# cp image-bundle.tar /var/lib/k0s/images/image-bundle.tarVia k0sctl
As an alternative to the previous step, you can use k0sctl to upload image
bundles to worker nodes. k0sctl can also be used to upload the k0s binary file
to all nodes. Take a look at this example configuration with one controller and
one worker node to upload k0s binary and an image bundle:
apiVersion: k0sctl.k0sproject.io/v1beta1
kind: ClusterConfig
metadata:
name: k0s
spec:
k0s:
version: {{{ k0s_version }}}
hosts:
- role: controller
ssh:
address: <controller-ip-address>
user: ubuntu
keyPath: /path/to/.ssh/id_rsa
# uploadBinary: <boolean>
# When true the k0s binaries are cached and uploaded
# from the host running k0sctl instead of downloading
# directly to the target host.
uploadBinary: true
# k0sBinaryPath: <local filepath>
# Upload a custom or manually downloaded k0s binary
# from a local path on the host running k0sctl to the
# target host.
# k0sBinaryPath: path/to/k0s_binary/k0s
- role: worker
ssh:
address: <worker-ip-address>
user: ubuntu
keyPath: /path/to/.ssh/id_rsa
uploadBinary: true
files:
# This airgap bundle file will be uploaded from the k0sctl
# host to the specified directory on the target host
- src: /path/to/airgap-bundle-amd64.tar
dstDir: /var/lib/k0s/images
perm: 0755Disable image pulling (optional)
Use the following k0s configuration to ensure that all pods and pod templates
managed by k0s contain an imagePullPolicy of Never, ensuring that no images
are pulled from the Internet at any time.
apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig
metadata:
name: k0s
spec:
images:
default_pull_policy: NeverAutopilot Multicommand (`docs/autopilot-multicommand.md`)
**Multi-Command Plans**
Autopilot relies on a Plan for defining the Commands that should be
executed, the Signal Nodes that each should be run on, and the status of
each Command.
A Plan:
- Defines one or many Commands that specify what actions should be performed.
- Specifies how Signal Nodes should be discovered per-Command.
- Saves the status of the Plan execution by resolved Signal Nodes
A Command:
- An instructional step inside of a Plan that is applied against a Signal Node
A Signal Node:
- Any node (controller or worker) that can receive updates with Autopilot.
**Execution**
The execution of a Plan is the result of processing Commands through
a number of Processing States.
When a Plan is executed, each of the Commands are executed in the order
of their appearance in the Plan.
- A Plan transitions to the next Command only when the current Command posts
a state of Completed. - Any Command that posts one of the recognized Error States will result in the
current Command and Plan to abort processing.- The status of the Command and Plan will reflect this.
- A Plan is considered finished only when all of its defined Commands post a
Completed state.
**Status**
The progress and state of each Command is recorded in the Plan status.
- Every Command in the Plan has an associated status entry with the same index
as the Command- i.e. The second Command in a Plan has an index of
1, and so does its status.
- i.e. The second Command in a Plan has an index of
- The status of all Commands is taken into consideration when determining if the
Plan is finished.
**Example**
The following is an example of a Plan that has been applied as is currently being
processed by autopilot.
(line numbers added for commentary below)
/* Detailed source-code truncated for AI context efficiency. */- Lines 7-33 are the two Commands that make up this plan - an
airgapupdateandk0supdate. - Lines 38-55 are the associated status entries for the two Commands.
The state of this Plan excerpt is that autopilot has successfully processed the Plan, and
has begun processing the airgapupdate Command. Its status indicates SignalSent which means
that the Signal Node has been sent signaling information to perform an air gapped update.
**Processing States**
The following are the various states that both Plans and Commands
adhere to.
stateDiagram-v2
[*]-->NewPlan
NewPlan-->SchedulableWait
NewPlan-->Errors***
SchedulableWait-->Schedulable
SchedulableWait-->Completed
Schedulable-->SchedulableWait
Errors***-->[*]
Completed-->[*]Note that the Errors* state is elaborated in detail below in Error States.
**NewPlan**
When a Plan is created with the name autopilot, the NewPlan state
processing takes effect.
It is the responsibility of NewPlan to ensure that the status of all
the Commands are represented in the Plan status. This Plan status
is needed at later points in Plan processing to determine if the entire
Plan is completed.
The main difference between NewPlan and all the other states is that
NewPlan will iterate over all commands; the other states deal with the
active command.
**SchedulableWait**
Used to evaluate a Command to determine if it can be scheduled for processing.
If the Command is determined that it can be processed, the state is set to
Schedulable.
**Schedulable**
The Schedulable state is set by SchedulableWait to indicate that this
command should execute. The execution of a Command in this state will be
whichever logic is defined by the Command.
The ending of this state should either transition to SchedulableWait for
further processing + completion detection, or transition to an error.
**Completed**
The Completed state indicates that the command has finished processing.
Once a plan/command are in the Completed state, no further processing
will occur on this plan/command.
**Error States**
When a plan or command processing goes into one of the designated error
states, this is considered fatal and the plan/command processing will
terminate.
Error states are generally defined by the Command implementation.
The core autopilot functionality is only interested when in the
4 core states (NewPlan, SchedulableWait, Schedulable, Completed),
and treats all other states as an error.
flowchart TD
Errors --> IncompleteTargets
Errors --> Restricted
Errors -. no longer in use .-> MissingSignalNode| Error State | Command | States | Description |
|---|---|---|---|
| IncompleteTargets | airgapupdate, k0supdate |
NewPlan, Schedulable | Indicates that a Signal Node that existed during the discover phase in NewPlan no longer exists (ie. no ControlNode or Node object) |
| Restricted | airgapupdate, k0supdate |
NewPlan | Indicates that a Plan has requested an update of a Signal Node type that contradicts the startup exclusions (the --exclude-from-plans argument) |
airgapupdate, k0supdate |
Schedulable | Legacy state used by older k0s releases to indicate IncompleteTargets; no longer in use. |
**Sequence: Example**
Using the example above as a reference, this outlines the basic sequence of events of
state transitions to the operations performed on each object.
/* Detailed source-code truncated for AI context efficiency. */Autopilot (`docs/autopilot.md`)
Autopilot
A tool for updating your k0s controller and worker nodes using specialized plans.
There is a public update-server hosted on the same domain as the documentation site. See the example below on how to use it. There is only a single channel edge_release available. The channel exposes the latest released version.
How it works
- You create a
PlanYAML- Defining the update payload (new version of
k0s, URLs for platforms, etc.) - Add definitions for all the nodes that should receive the update.
- Either statically, or dynamically using label/field selectors
- Defining the update payload (new version of
- Apply the
Plan- Applying a
Planis a simplekubectl applyoperation.
- Applying a
- Monitor the progress
- The applied
Planprovides a status that details the progress.
- The applied
Automatic updates
To enable automatic updates, create an UpdateConfig object:
apiVersion: autopilot.k0sproject.io/v1beta2
kind: UpdateConfig
metadata:
name: example
namespace: default
spec:
channel: edge_release
updateServer: https://updates.k0sproject.io/
upgradeStrategy:
type: periodic
periodic:
# The following fields configures updates to happen only on Tue or Wed at 13:00-15:00
days: [Tuesdays,Wednesday]
startTime: "13:00"
length: 2h
planSpec: # This defines the plan to be created IF there are updates available
...Safeguards
There are a number of safeguards in place to avoid breaking a cluster.
Stateless Component
- The autopilot component were designed to not require any heavy state, or
massive synchronization. Controllers can disappear, and backup controllers can
resume the autopilot operations.
Workers Update Only After Controllers
The versioning that Kubelet and the Kubernetes API server adhere to requires that Kubelets
should not be of a newer version than the API server.How autopilot handles this is that when a
Planis applied that has both controller
and worker nodes, all the controller nodes will be updated first. It is only when
all controllers have updated successfully that worker nodes will receive their
update instructions.
Plans are Immutable
- When you apply a
Plan, autopilot evaluates all of the controllers and workers that
should be included into thePlan, and tracks them in the status. After this point, no
additional changes to the plan (other than status) will be recognized.- This helps in largely dynamic worker node environments where nodes that may have been
matched by theselectordiscovery method no longer exist by the time the update
is ready to be scheduled.
- This helps in largely dynamic worker node environments where nodes that may have been
Controller Quorum Safety
- Prior to scheduling a controller update, autopilot queries the API server of all
controllers to ensure that they report a successful/ready. - Only once all controllers are
/readywill the current controller get sent update signaling. - If a previously discovered controller target can no longer be resolved, the
Plantransitions
into anIncompleteTargetsstate and execution ends. Transient readiness failures requeue.
Controllers Update Sequentially
- Despite having the configuration options for controllers to set concurrency, only one
controller will be updated at a time.
Update Payload Verification
- Each
updateobject payload can provide an optionalsha256hash of the update content
(specified inurl), which is compared against the update content after it downloads.
Configuration
Autopilot relies on a Plan object on its instructions on what to update.
Here is an arbitrary Autopilot plan:
apiVersion: autopilot.k0sproject.io/v1beta2
kind: Plan
metadata:
name: autopilot
spec:
id: id1234
timestamp: now
commands:
- k0supdate:
version: {{{ k0s_version }}}
platforms:
linux-amd64:
url: https://github.com/k0sproject/k0s/releases/download/{{{ k0s_version }}}/k0s-{{{ k0s_version }}}-amd64
sha256: '0000000000000000000000000000000000000000000000000000000000000000'
targets:
controllers:
discovery:
static:
nodes:
- ip-172-31-44-131
- ip-172-31-42-134
- ip-172-31-39-65
workers:
limits:
concurrent: 5
discovery:
selector:
labels: environment=staging
fields: metadata.name=worker2Core Fields
`apiVersion (required)`
- The current version of the Autopilot API is
v1beta2, with a full group-version ofautopilot.k0sproject.io/v1beta2
`metadata.name (required)`
- The name of the plan should always be
autopilot- Note: Plans will not execute if they don't follow this convention.
Spec Fields
`spec.id (optional)`
- An identifier that can be provided by the creator for informational and tracking purposes.
`spec.timestamp (optional)`
- A timestamp value that can be provided by the creator for informational purposes. Autopilot
does nothing with this information.
`spec.commands[] (required)`
- The
commandscontains the commands that should be performed as a part of the plan.
**`k0supdate`** Command
`spec.commands[].k0supdate.version (required)`
- The version of the binary being updated. This version is used to compare against the installed
version before and after update to ensure success.
`spec.commands[].k0supdate.platforms.*.url (required)`
- A URL providing where the updated binary should be downloaded from, for this specific platform.
- The naming of platforms is a combination of
$GOOSand$GOARCH, separated by a hyphen (-)- e.g.:
linux-amd64,linux-arm64,linux-arm
- e.g.:
- Note: The main supported platform is
linux. Autopilot may work on other platforms, however
this has not been tested.
- The naming of platforms is a combination of
`spec.commands[].k0supdate.platforms.*.sha256 (optional)`
- If a SHA256 hash is provided for the binary, the completed downloaded will be verified against it.
`spec.commands[].k0supdate.targets.controllers
- This object provides the details of how
controllersshould be updated.
`spec.commands[].k0supdate.targets.controllers.limits.concurrent (fixed as 1)`
- The configuration allows for specifying the number of concurrent controller updates
through the plan spec, however for controller targets this is fixed always to1. - By ensuring that only one controller updates at a time, we aim to avoid scenarios
where quorom may be disrupted.
`spec.commands[].k0supdate.targets.workers
- This object provides the details of how
workersshould be updated.
`spec.commands[].k0supdate.targets.workers.limits.concurrent (optional, default = 1)`
- Specifying a
concurrentvalue for worker targets will allow for that number of workers
to be updated at a time. If no value is provided,1is assumed.
**`airgapupdate`** Command
`spec.commands[].airgapupdate.version (required)`
- The version of the airgap bundle being updated.
`spec.commands[].airgapupdate.platforms.*.url (required)`
- An URL providing where the updated binary should be downloaded from, for this specific platform.
- The naming of platforms is a combination of
$GOOSand$GOARCH, separated by a hyphen (-)- eg:
linux-amd64,linux-arm64,linux-arm
- eg:
- Note: The main supported platform is
linux. Autopilot may work on other platforms, however
this has not been tested.
- The naming of platforms is a combination of
`spec.commands[].airgapupdate.platforms.*.sha256 (optional)`
- If a SHA256 hash is provided for the binary, the completed downloaded will be verified against it.
`spec.commands[].airgapupdate.workers
- This object provides the details of how
workersshould be updated.
`spec.commands[].airgapupdate.workers.limits.concurrent (optional, default = 1)`
- Specifying a
concurrentvalue for worker targets will allow for that number of workers
to be updated at a time. If no value is provided,1is assumed.
Static Discovery
This defines the static discovery method used for this set of targets (controllers, workers). The static discovery method relies on a fixed set of hostnames defined
in .nodes.
It is expected that a Node (workers) or ControlNode (controllers) object exists with
the same name.
static:
nodes:
- ip-172-31-44-131
- ip-172-31-42-134
- ip-172-31-39-65`spec.commands[].k0supdate.targets.*.discovery.static.nodes[] (required for static)`
- A list of hostnames that should be included in target set (
controllers,workers).
Selector Target Discovery
The selector target discovery method relies on a dynamic query to the Kubernetes API
using labels and fields to produce a set of hosts that should be updated.
Providing both labels and fields in the selector definition will result in a logical AND of both operands.
selector:
labels: environment=staging
fields: metadata.name=worker2Specifying an empty selector will result in all nodes being selected for this target set.
selector: {}`spec.commands[].k0supdate.targets.*.discovery.selector.labels (optional)`
- A collection of name/value labels that should be used for finding the appropriate nodes
for the update of this target set.
`spec.commands[].k0supdate.targets.*.discovery.selector.fields (optional)`
- A collection of name/value fields that should be used for finding the appropriate nodes
for the update of this target set.- Note: Currently only the field
metadata.nameis available as a query field.
- Note: Currently only the field
Status Reporting
After a Plan has been applied, its progress can be viewed in the .status of theautopilot Plan.
kubectl get plan autopilot -oyamlAn example of a Plan status:
status:
state: SchedulableWait
commands:
- state: SchedulableWait
k0supdate:
controllers:
- lastUpdatedTimestamp: "2022-04-07T15:52:44Z"
name: controller0
state: SignalCompleted
- lastUpdatedTimestamp: "2022-04-07T15:52:24Z"
name: controller1
state: SignalCompleted
- lastUpdatedTimestamp: "2022-04-07T15:52:24Z"
name: controller2
state: SignalPending
workers:
- lastUpdatedTimestamp: "2022-04-07T15:52:24Z"
name: worker0
state: SignalPending
- lastUpdatedTimestamp: "2022-04-07T15:52:24Z"
name: worker1
state: SignalPending
- lastUpdatedTimestamp: "2022-04-07T15:52:24Z"
name: worker2
state: SignalPendingTo read this status, this indicates that:
- The overall status of the update is
SchedulableWait, meaning that autopilot is
waiting for the next opportunity to process a command. - There are three controller nodes
- Two controllers have
SignalCompletedsuccessfully - One is waiting to be signaled (
SignalPending)
- Two controllers have
- There are also three worker nodes
- All are awaiting signaling updates (
SignalPending)
- All are awaiting signaling updates (
Plan Status
The Plan status at .status.status represents the overall status of the autopilot
update operation. There are a number of statuses available:
| Status | Description | Ends Plan? |
|---|---|---|
IncompleteTargets |
There are nodes in the resolved Plan that do not have associated Node (worker) or ControlNode (controller) objects. |
Yes |
Schedulable |
Indicates that the Plan can be re-evaluated to determine which next node to update. |
No |
SchedulableWait |
Scheduling operations are in progress, and no further update scheduling should occur. | No |
Completed |
The Plan has run successfully to completion. |
Yes |
Restricted |
The Plan included node types (controller or worker) that violates the --exclude-from-plans restrictions. |
Yes |
Node Status
Similar to the Plan Status, the individual nodes can have their own statuses:
| Status | Description |
|---|---|
SignalPending |
The node is available and awaiting an update signal |
SignalSent |
Update signaling has been successfully applied to this node. |
SignalMissingPlatform |
This node is a platform that an update has not been provided for. |
SignalMissingNode |
This node does have an associated Node (worker) or ControlNode (controller) object. |
UpdateConfig
UpdateConfig Core Fields
`apiVersion (required field)`
- API version. The current version of the Autopilot API is
v1beta2, with a full group-version ofautopilot.k0sproject.io/v1beta2
`metadata.name (required field)`
- Name of the config.
Spec
`spec.channel (optional)`
- Update channel to use. Supported values:
stable(default),unstable.
`spec.updateServer (optional)`
- Update server url. Defaults to
https://updates.k0sproject.io
`spec.upgradeStrategy.type `
- Select which update strategy to use.
`spec.upgradeStrategy.cron (optional)` **DEPRECATED**
- Schedule to check for updates in crontab format.
`spec.upgradeStrategy.cron
Fields:
days: On which weekdays to check for updatesstartTime: At which time of day to check updateslength: The length of the update window
`spec.planSpec (optional)`
- Describes the behavior of the autopilot generated
Plan
Example
apiVersion: autopilot.k0sproject.io/v1beta2
kind: UpdateConfig
metadata:
name: example
spec:
channel: stable
updateServer: https://updates.k0sproject.io/
upgradeStrategy:
type: periodic
periodic:
# The following fields configures updates to happen only on Tue or Wed at 13:00-15:00
days: [Tuesdays,Wednesday]
startTime: "13:00"
length: 2h
# Optional. Specifies a created Plan object
planSpec:
commands:
- k0supdate: # optional
forceupdate: true # optional
targets:
controllers:
discovery:
static:
nodes:
- ip-172-31-44-131
- ip-172-31-42-134
- ip-172-31-39-65
workers:
limits:
concurrent: 5
discovery:
selector:
labels: environment=staging
fields: metadata.name=worker2
airgapupdate: # optional
workers:
limits:
concurrent: 5
discovery:
selector:
labels: environment=staging
fields: metadata.name=worker2FAQ
Q: How do I apply the `Plan` and `ControlNode` CRDs?
A: These CRD definitions are embedded in the k0s binary and applied on startup.
No additional action is needed.
Q: How will `ControlNode` instances get removed?
A: ControlNode instances are created by autopilot controllers as they startup. When
controllers disappear, they will not remove their associated ControlNode instance. It
is the responsibility of the operator/administrator to ensure their maintenance.
Q: I upgraded my workers, and now Kubelets are no longer reporting
You probably upgraded your workers to an API version greater than what is
available on the API server.
https://kubernetes.io/releases/version-skew-policy/
Make sure that your controllers are at the desired version first before
upgrading workers.
Backup (`docs/backup.md`)
Backup/Restore overview
k0s has integrated support for backing up cluster state and configuration. The k0s backup utility is aiming to back up and restore k0s managed parts of the cluster.
The backups created by k0s backup command have following pieces of your cluster:
- certificates (the content of the
<data-dir>/pkidirectory) - etcd snapshot, if the etcd data store is used
- Kine/SQLite snapshot, if the Kine/SQLite data store is used
- k0s.yaml
- any custom defined manifests under the
<data-dir>/manifests - any image bundles located under the
<data-dir>/images - any helm configuration
Parts NOT covered by the backup utility:
PersistentVolumesof any running application- data store, in case something else than etcd or Kine/SQLite is used
- any configuration to the cluster introduced by manual changes (e.g. changes that weren't saved under the
<data-dir>/manifests)
Any of the backup/restore related operations MUST be performed on the controller node.
Backup/restore a k0s node locally
Backup (local)
To create backup run the following command on the controller node:
k0s backup --save-path=<directory>The directory used for the save-path value must exist and be writable. The default value is the current working directory.
The command provides backup archive using following naming convention: k0s_backup_.tar.gz
Because of the date/time usage, it is guaranteed that none of the previously created archives would be overwritten.
To output the backup archive to standard output, use - as the save path.
Restore (local)
To restore cluster state from the archive use the following command on the controller node:
k0s restore /tmp/k0s_backup_2021-04-26T19_51_57_000Z.tar.gzThe command would fail if the data directory for the current controller has overlapping data with the backup archive content.
The command would use the archived k0s.yaml as the cluster configuration description.
It is written to the current working directory as k0s_<archive timestamp>.yaml
(use --config-out to change this).
If the backup is restored onto a node with a different IP address than the one
it was taken on, and the archived configuration pins the etcd peer address viaspec.storage.etcd.peerAddress, adjust it before starting the controller:
either set it to the new node's address or remove it to have it detected
automatically. Then start the controller with --config pointing at the
adjusted file. k0s will update the peer URL of the restored etcd member
accordingly on startup. This only works as long as the restored controller is
the sole etcd member, i.e. before any additional controllers are joined.
In case if your cluster is HA, after restoring single controller node, join the rest of the controller nodes to the cluster.
E.g. steps for N nodes cluster would be:
- Restore backup on fresh machine
- Run controller there
- Join N-1 new machines to the cluster the same way as for the first setup.
To read the backup archive from standard input, use - as the file path.
Encrypting backups (local)
By using - as the save or restore path, it is possible to pipe the backup archive through an encryption utility such as GnuPG or OpenSSL.
Note that unencrypted data will still briefly exist as temporary files on the local file system during the backup archive generation.
Encrypting backups using GnuPG
Follow the instructions for your operating system to install the gpg command if it is not already installed.
This tutorial only covers the bare minimum for example purposes. For secure key management practices and advanced usage refer to the GnuPG user manual.
To generate a new key-pair, use:
gpg --gen-keyThe key will be stored in your key ring.
gpg --list-keysThis will output a list of keys:
/home/user/.gnupg/pubring.gpg
------------------------------
pub 4096R/BD33228F 2022-01-13
uid Example User <[email protected]>
sub 4096R/2F78C251 2022-01-13To export the private key for decrypting the backup on another host, note the key ID (BD33228F in this example) in the list and use:
gpg --export-secret-keys --armor BD33228F > k0s.keyTo create an encrypted k0s backup:
k0s backup --save-path - | gpg --encrypt --recipient [email protected] > backup.tar.gz.gpgRestoring encrypted backups using GnuPG
You must have the private key in your GnuPG keychain. To import the key that was exported in the previous example, use:
gpg --import k0s.keyTo restore the encrypted backup, use:
gpg --decrypt backup.tar.gz.gpg | k0s restore -Backup/restore a k0s cluster using k0sctl
With k0sctl you can perform cluster level backup and restore remotely with one command.
Backup (remote)
To create backup run the following command:
k0sctl backupk0sctl connects to the cluster nodes to create a backup. The backup file is stored in the current working directory.
Restore (remote)
To restore cluster state from the archive use the following command:
k0sctl apply --restore-from /path/to/backup_file.tar.gzThe control plane load balancer address (externalAddress) needs to remain the same between backup and restore. This is caused by the fact that all worker node components connect to this address and cannot currently be re-configured.
Cis Benchmark (`docs/cis_benchmark.md`)
Kube-bench Security Benchmark
Kube-bench is an open source tool which can be used to verify security best practices as defined in CIS Kubernetes Benchmark. It provides a number of tests to help harden your k0s clusters. By default, k0s will pass Kube-bench benchmarks with some exceptions, which are shown below.
Run
Follow the Kube-bench quick start instructions.
After installing the Kube-bench on the host that is running k0s cluster run the following command:
kube-bench run --config-dir docs/kube-bench/cfg/ --benchmark k0s-1.0Summary of disabled checks
Master Node Security Configuration
The current configuration has in total 8 master checks disabled:
id: 1.2.10 - EventRateLimit requires external yaml config. It is left for the users to configure it
yamltype: skip text: "Ensure that the admission control plugin EventRateLimit is set (Manual)"id: 1.2.12 - By default this isn't passed to the apiserver for air-gap functionality
yamltype: skip text: "Ensure that the admission control plugin AlwaysPullImages is set (Manual)"id: 1.2.22 - For sake of simplicity of k0s all audit configurations are skipped. It is left for the users to configure it
yamltype: skip text: "Ensure that the --audit-log-path argument is set (Automated)"id: 1.2.23 - For sake of simplicity of k0s all audit configuration are skipped. It is left for the users to configure it
yamltype: skip text: "Ensure that the --audit-log-maxage argument is set to 30 or as appropriate (Automated)"id: 1.2.24 - For sake of simplicity of k0s all audit configurations are skipped. It is left for the users to configure it
yamltype: skip text: "Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriate (Automated)"id: 1.2.25 - For sake of simplicity of k0s all audit configurations are skipped. It is left for the users to configure it
yamltype: skip text: "Ensure that the --audit-log-maxsize argument is set to 100 or as appropriate (Automated)"id: 1.2.33 - By default it is not enabled. Left for the users to decide
yamltype: skip text: "Ensure that the --encryption-provider-config argument is set as appropriate (Manual)"id: 1.2.34 - By default it is not enabled. Left for the users to decide
yamltype: skip text: "Ensure that encryption providers are appropriately configured (Manual)"
Worker Node Security Configuration
and 4 node checks disabled:
id: 4.1.1 - not applicable since k0s does not use kubelet service file
yamltype: skip text: "Ensure that the kubelet service file permissions are set to 644 or more restrictive (Automated)"id: 4.1.2 - not applicable since k0s does not use kubelet service file
yamltype: skip text: "Ensure that the kubelet service file ownership is set to root:root (Automated)"id: 4.2.6 - k0s does not set this. See https://github.com/kubernetes/kubernetes/issues/66693
yamltype: skip text: "Ensure that the --protect-kernel-defaults argument is set to true (Automated)"id: 4.2.10 - k0s doesn't set this up because certs get auto rotated
yamltype: skip text: "Ensure that the --tls-cert-file and --tls-private-key-file arguments are set as appropriate (Manual)"
Control Plane Configuration
3 checks for the control plane:
id: 3.1.1 - For purpose of being fully automated k0s is skipping this check
yamltype: skip text: "Client certificate authentication should not be used for users (Manual)"id: 3.2.1 - out-of-the box configuration does not have any audit policy configuration but users can customize it in spec.api.extraArgs section of the config
yamltype: skip text: "Ensure that a minimal audit policy is created (Manual)"id: 3.2.2 - Same as previous
yamltype: skip text: "Ensure that the audit policy covers key security concerns (Manual)"
Kubernetes Policies
Policy checks are also disabled. The checks are manual and are up to the end user to decide on them.
Cloud Providers (`docs/cloud-providers.md`)
Cloud providers
K0s supports all Kubernetes cloud controllers. However, those must be installed as separate cluster add-ons since k0s builds Kubernetes components in providerless mode.
Enable cloud provider support in kubelet
You must enable cloud provider mode for kubelet. To do this, run the workers with --enable-cloud-provider=true.
When deploying with k0sctl, you can add this into the installFlags of worker hosts.
spec:
hosts:
- ssh:
address: 10.0.0.1
user: root
keyPath: ~/.ssh/id_rsa
installFlags:
- --enable-cloud-provider
- --kubelet-extra-args="--cloud-provider=external"
role: workerDeploy the cloud provider
You can use any means to deploy your cloud controller into the cluster. Most providers support Helm charts to deploy them.
Note: The prerequisites for the various cloud providers can vary (for example, several require that configuration files be present on all of the nodes). Refer to your chosen cloud provider's documentation as necessary.
k0s Cloud Provider
Alternatively, k0s provides its own lightweight cloud provider that can be used to statically assign ExternalIP values to worker nodes via Kubernetes annotations. This is beneficial for those who need to expose worker nodes externally via static IP assignments.
To enable this functionality, add the parameter --enable-k0s-cloud-provider=true to all controllers, and --enable-cloud-provider=true to all workers.
Adding a static IP address to a node using kubectl:
kubectl annotate \
node <node> \
k0sproject.io/node-ip-external=<external IP>[,<external IP 2>][,<external IP 3>]Both IPv4 and IPv6 addresses and multiple comma-separated values are supported.
Defaults
The default node refresh interval is 2m, which can be overridden using the --k0s-cloud-provider-update-frequency=<duration> parameter when launching the controller(s).
The default port that the cloud provider binds to can be overridden using the --k0s-cloud-provider-port= parameter when launching the controller(s).
Commercial Support (`docs/commercial-support.md`)
Commercial support
Commercial support for k0s if offered by Mirantis Inc..
Mirantis can provide various different levels of support starting from DevCare (9-to-5) all the way to OpsCare+ with fully managed service.
On top of our normal release and support model our commercial customers have access to critical security patches even for released versions that fall outside of the Open Source maintained releases.[^1] Commercial support also includes support for k0s related tooling such as k0sctl.
If you are interested in commercial support for k0s check out our support description and please contact us for further details.
[^1]: This is assuming there is a compatible release of upstream project with the fix
Compose Cluster.Yaml (`docs/compose-cluster.yaml`)
name: compose-cluster
x-k0s-image: &k0s-image docker.io/k0sproject/k0s:{{{ k0s_docker_version }}}
x-k0s-controller: &k0s-controller
image: *k0s-image
networks:
- k0s-net
tmpfs:
- /run # this is where k0s stores runtime data
- /tmp
configs:
- source: k0s.yaml
target: /etc/k0s/k0s.yaml
labels:
- traefik.enable=true
- traefik.tcp.routers.kube-api.service=kube-api
- traefik.tcp.routers.kube-api.rule=HostSNI(*)
- traefik.tcp.routers.kube-api.entrypoints=kube-api
- traefik.tcp.services.kube-api.loadbalancer.server.port=6443
- traefik.tcp.routers.k0s-api.service=k0s-api
- traefik.tcp.routers.k0s-api.rule=HostSNI(*)
- traefik.tcp.routers.k0s-api.entrypoints=k0s-api
- traefik.tcp.services.k0s-api.loadbalancer.server.port=9443
- traefik.tcp.routers.konnectivity.service=konnectivity
- traefik.tcp.routers.konnectivity.rule=HostSNI(*)
- traefik.tcp.routers.konnectivity.entrypoints=konnectivity
- traefik.tcp.services.konnectivity.loadbalancer.server.port=8132
restart: on-failure
x-k0s-worker: &k0s-worker
image: *k0s-image
networks:
- k0s-net
depends_on:
- k0s-controller-1
command: [k0s, worker, --token-file, /run/secrets/k0sproject.io/tokens/worker]
volumes:
- /var/lib/k0s # this is where k0s stores its data
- /var/log/pods # this is where k0s stores pod logs
- /dev/kmsg:/dev/kmsg:ro # required by kubelets OOM watcher
- k0s-worker-token:/run/secrets/k0sproject.io/tokens:ro
tmpfs:
- /run # this is where k0s stores runtime data
- /tmp
devices:
- /dev/kmsg # required by kubelet's OOM watcher
cap_add:
- sys_admin
- net_admin
- sys_ptrace
- sys_resource
- syslog
security_opt:
- seccomp:unconfined # allow access to the session keyring
restart: on-failure
networks:
k0s-net:
driver: bridge
services:
k0s-lb:
image: docker.io/library/traefik:v3.7.14
container_name: k0s-lb
hostname: k0s-lb
networks:
- k0s-net
command:
- --api.insecure=true
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entryPoints.kube-api.address=:6443
- --entryPoints.k0s-api.address=:9443
- --entryPoints.konnectivity.address=:8132
ports:
- 6443:6443 # publish the Kubernetes API server port
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
k0s-controller-1:
<<: *k0s-controller
container_name: k0s-controller-1
hostname: k0s-controller-1
depends_on:
- k0s-lb
command: [k0s, controller]
post_start:
- command:
- /bin/sh
- -euc
- |
bootstrap() {
# this works even if etcd isn't up
find /run/secrets/k0sproject.io/controller-token ! -path /run/secrets/k0sproject.io/controller-token -prune -exec rm -rf {} +
k0s token pre-shared --role controller \
--cert /var/lib/k0s/pki/ca.crt \
--url https://k0s-lb:9443 \
--out /run/secrets/k0sproject.io/controller-token/
find /run/secrets/k0sproject.io/worker-token ! -path /run/secrets/k0sproject.io/worker-token -prune -exec rm -rf {} +
k0s token pre-shared --role worker \
--cert /var/lib/k0s/pki/ca.crt \
--url https://k0s-lb:6443 \
--out /run/secrets/k0sproject.io/worker-token/
mv /run/secrets/k0sproject.io/controller-token/*.yaml /var/lib/k0s/manifests/k0s-token-secrets/controller.yaml
mv /run/secrets/k0sproject.io/controller-token/token_* /run/secrets/k0sproject.io/controller-token/controller
mv /run/secrets/k0sproject.io/worker-token/*.yaml /var/lib/k0s/manifests/k0s-token-secrets/worker.yaml
mv /run/secrets/k0sproject.io/worker-token/token_* /run/secrets/k0sproject.io/worker-token/worker
}
while [ ! -f /var/lib/k0s/pki/ca.crt ] || ! bootstrap; do
sleep 1
done
sleep 10 # give this controller a bit of a head start
volumes:
- /var/lib/k0s # this is where k0s stores its data
- k0s-token-secrets:/var/lib/k0s/manifests/k0s-token-secrets
- k0s-controller-token:/run/secrets/k0sproject.io/controller-token
- k0s-worker-token:/run/secrets/k0sproject.io/worker-token k0s-controller-2: &k0s-additional-controller
<<: *k0s-controller
container_name: k0s-controller-2
hostname: k0s-controller-2
depends_on:
- k0s-controller-1
command:
[
k0s,
controller,
--token-file,
/run/secrets/k0sproject.io/tokens/controller,
]
volumes:
- /var/lib/k0s # this is where k0s stores its data
- k0s-token-secrets:/var/lib/k0s/manifests/k0s-token-secrets:ro
- k0s-controller-token:/run/secrets/k0sproject.io/tokens:ro
k0s-controller-3:
<<: *k0s-additional-controller
container_name: k0s-controller-3
hostname: k0s-controller-3
k0s-worker-1:
<<: *k0s-worker
container_name: k0s-worker-1
hostname: k0s-worker-1
k0s-worker-2:
<<: *k0s-worker
container_name: k0s-worker-2
hostname: k0s-worker-2
k0s-worker-3:
<<: *k0s-worker
container_name: k0s-worker-3
hostname: k0s-worker-3
volumes:
k0s-token-secrets:
driver: local
driver_opts:
type: tmpfs
device: tmpfs
k0s-controller-token:
driver: local
driver_opts:
type: tmpfs
device: tmpfs
k0s-worker-token:
driver: local
driver_opts:
type: tmpfs
device: tmpfs
configs:
k0s.yaml:
content: |
spec:
api:
externalAddress: k0s-lb
Compose.Yaml (`docs/compose.yaml`)
services:
k0s-controller:
image: docker.io/k0sproject/k0s:{{{ k0s_docker_version }}}
container_name: k0s-controller
hostname: k0s-controller
network_mode: bridge # other modes are unsupported
ports:
- 6443:6443 # publish the Kubernetes API server port
volumes:
- /var/lib/k0s # this is where k0s stores its data
- /var/log/pods # this is where k0s stores pod logs
- /dev/kmsg:/dev/kmsg:ro # required by kubelets OOM watcher
tmpfs:
- /run # this is where k0s stores runtime data
devices:
- /dev/kmsg # required by kubelet's OOM watcher
cap_add:
- sys_admin
- net_admin
- sys_ptrace
- sys_resource
- syslog
security_opt:
- seccomp:unconfined # allow access to the session keyring
configs:
- source: k0s.yaml
target: /etc/k0s/k0s.yaml
configs:
k0s.yaml:
content: |
apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig
metadata:
name: k0s
spec:
storage:
type: kine
# Any additional configuration goes here ...
Configuration Validation (`docs/configuration-validation.md`)
Configuration validation
k0s command-line interface has the ability to validate config syntax:
k0s config validate --config path/to/config/fileconfig validate sub-command can validate the following:
- YAML formatting
- SAN addresses
- Network providers
- Worker profiles
Configuration (`docs/configuration.md`)
Configuration options
Using a configuration file
k0s can be installed without a config file. In that case the default configuration will be used. You can, though, create and run your own non-default configuration (used by the k0s controller nodes).
k0s supports providing only partial configurations. In case of partial configuration is provided, k0s will use the defaults for any missing values.
Generate a YAML config file that uses the default settings.
shellmkdir -p /etc/k0s k0s config create > /etc/k0s/k0s.yamlModify the new YAML config file according to your needs, refer to Configuration file reference below. You can remove the default values if wanted as k0s supports partial configs too.
Install k0s with your new config file.
shellsudo k0s install controller -c /etc/k0s/k0s.yamlIf you need to modify your existing configuration later on, you can change your config file also when k0s is running, but remember to restart k0s to apply your configuration changes.
shellsudo k0s stop sudo k0s start
Configuring k0s via k0sctl
k0sctl can deploy your configuration options at cluster creation time. Your
options should be placed in the spec.k0s.config section of the k0sctl
configuration file. See the section on how to install k0s via
k0sctl and the k0sctl README for more information.
Configuration file reference
CAUTION: As many of the available options affect items deep in the stack, you should fully understand the correlation between the configuration file components and your specific environment before making any changes.
A YAML config file follows, with defaults as generated by the k0s config create command:
/* Detailed source-code truncated for AI context efficiency. */`spec` Key Detail
`spec.api`
| Element | Description |
|---|---|
address |
IP Address used by cluster components to talk to the API server. Also serves as one of the addresses pushed on the k0s create service certificate on the API. Defaults to first non-local address found on the node. |
onlyBindToAddress |
The API server binds to all interfaces by default. With this option set to true, the API server will only listen on the IP address configured by the address option (first non-local address by default). This can be necessary with multi-homed control plane nodes. |
externalAddress |
The load balancer address (for k0s controllers running behind a load balancer). Configures all cluster components to connect to this address and configures this address for use when joining new nodes to the cluster. |
sans |
List of additional addresses to push to API servers serving the certificate. |
ca.expiresAfter |
The expiration duration of the CA certificate (default: 87600h) |
ca.certificatesExpireAfter |
The expiration duration of the server certificate (default: 8760h) |
extraArgs |
Map of key-values (strings) for any extra arguments to pass down to Kubernetes API server process. extraArgs are recommended over rawArgs if the use case allows it. Any behavior triggered by these parameters is outside k0s support. (default: empty) |
rawArgs |
Slice of strings for any raw arguments to pass down to the kube-apiserver process. These are appended after extraArgs. If possible, it's recommended to use extraArgs over rawArgs. Any behavior triggered by these parameters is outside k0s support. (default: empty) |
port¹ |
Custom port for the Kubernetes API server to listen on (default: 6443). When set to a privileged port (< 1024), k0s automatically grants the CAP_NET_BIND_SERVICE capability to the kube-apiserver process to allow binding to the port. |
k0sApiPort¹ |
Custom port for k0s API server to listen on (default: 9443) |
¹ If port and k0sApiPort are used with the externalAddress element, the load balancer serving at externalAddress must listen on the same ports.
`spec.storage`
| Element | Description |
|---|---|
type |
Type of the data store (valid values:etcd or kine). Note: Type etcd will cause k0s to create and manage an elastic etcd cluster within the controller nodes. |
etcd.peerAddress |
Node address used for etcd cluster peering. |
etcd.extraArgs |
Map of key-values (strings) for any extra arguments to pass down to etcd process. extraArgs are recommended over rawArgs if the use case allows it. Any behavior triggered by these parameters is outside k0s support. |
etcd.rawArgs |
Slice of strings for any raw arguments to pass down to the etcd process. These are appeneded after extraArg. If possible, it's recommended to use extraArgs over rawArgs. Any behavior triggered by these parameters is outside k0s support. |
etcd.ca.expiresAfter |
The expiration duration of the CA certificate (default: 87600h) |
etcd.ca.certificatesExpireAfter |
The expiration duration of the server certificate (default: 8760h) |
etcd.externalCluster |
Configuration when etcd is externally managed, i.e. running on dedicated nodes. See spec.storage.etcd.externalCluster |
kine.dataSource |
kine data source URL. |
kine.extraArgs |
Map of key-values (strings) for any extra arguments to pass down to kine process. extraArgs are recommended over rawArgs if the use case allows it. Any behavior triggered by these parameters is outside k0s support. |
kine.rawArgs |
Slice of strings for any raw arguments to pass down to the kine process. These are appended after extraArgs. Any behavior triggered by these parameters is outside k0s support. (default: empty) |
`spec.storage.etcd.externalCluster`
k0s can also work with an externally managed Etcd cluster. If this is configured, k0s will NOT set up etcd, it has to be managed manually.
| Element | Description |
|---|---|
endpoints |
Array of Etcd endpoints to use. |
etcdPrefix |
Prefix to use for this cluster. The same external Etcd cluster can be used for several k0s clusters, each prefixed with a unique prefix to store data with. |
caFile |
CaFile is the host path to a file with the Etcd cluster CA certificate. |
clientCertFile |
ClientCertFile is the host path to a file with the TLS certificate for etcd client. |
clientKeyFile |
ClientKeyFile is the host path to a file with the TLS key for etcd client. |
`spec.network`
| Element | Description |
|---|---|
provider |
Network provider (valid values: calico, kuberouter, or custom). For custom, you can push any network provider (default: kuberouter). Be aware that it is your responsibility to configure all the CNI-related setups, including the CNI provider itself and all necessary host levels setups (for example, CNI binaries). Note: Once you initialize the cluster with a network provider the only way to change providers is through a full cluster redeployment. The CNI files are applied as static manifests. |
podCIDR |
Pod network CIDR to use in the cluster by the network provider. Defaults to 10.244.0.0/16. |
serviceCIDR |
CNI-independent Network CIDR to use for cluster VIP services. Defaults to 10.96.0.0/12. |
primaryAddressFamily |
Defines the primary family for the cluster. Valid values are empty, IPv4, IPv6. If empty, K0s determines it based on .spec.API.ExternalAddress, if this isn't present it will use .spec.API.Address.. If both addresses are empty or the chosen address is a host name, defaults to IPv4. |
clusterDomain |
Cluster domain to be passed to the kubelet and the CoreDNS configuration. Defaults to cluster.local. |
`spec.network.calico`
| Element | Description |
|---|---|
mode |
Indicates the Calico backend to use. Either bird or vxlan (default). The deprecated legacy value ipip is also accepted. Defaults to vxlan with encapsulated overlay. |
overlay |
Overlay mode: Always (default), CrossSubnet or Never (requires mode=vxlan to disable Calico overlay-network, leaving pod CIDRs to be routed by the underlying network). |
vxlanPort |
The UDP port for VXLAN (default: 4789). |
vxlanVNI |
The virtual network ID for VXLAN (default: 4096). |
mtu |
MTU for overlay network (default: 0, which causes Calico to detect optimal MTU during bootstrap). |
wireguard |
Enable WireGuard-based encryption (default: false). Your host system must be WireGuard ready (refer to the Calico documentation for details). |
flexVolumeDriverPath |
The host path for Calico's flex-volume-driver(default: /usr/libexec/k0s/kubelet-plugins/volume/exec/nodeagent~uds). Change this path only if the default path cannot be used (refer to Project Calico Issue #2712 for details). Ideally, you will pair this option with a custom volumePluginDir in the profile you use for your worker nodes. |
ipAutodetectionMethod |
Used to force Calico to pick up the interface for pod network inter-node routing (default: "", meaning not set, so that Calico will instead use its defaults). For more information, refer to the Calico documentation. |
envVars |
Map of key-values (strings) for any calico-node environment variable. |
`spec.network.calico.envVars`
Environment variable's value must be string, e.g.:
spec:
network:
provider: calico
calico:
envVars:
TEST_BOOL_VAR: "true"
TEST_INT_VAR: "42"
TEST_STRING_VAR: testK0s runs Calico with some predefined vars, which can be overwritten by setting new value in spec.network.calico.envVars:
CALICO_IPV4POOL_CIDR: "{{ spec.network.podCIDR }}"
CALICO_DISABLE_FILE_LOGGING: "true"
FELIX_DEFAULTENDPOINTTOHOSTACTION: "ACCEPT"
FELIX_LOGSEVERITYSCREEN: "info"
FELIX_HEALTHENABLED: "true"
FELIX_PROMETHEUSMETRICSENABLED: "true"
FELIX_FEATUREDETECTOVERRIDE: "ChecksumOffloadBroken=true"FELIX_FEATUREDETECTOVERRIDE: ChecksumOffloadBroken=true disables VXLAN offloading because of projectcalico/calico#4727.
In single-stack mode, there are additional vars:
FELIX_IPV6SUPPORT: "false"In dual-stack mode, there are additional vars:
CALICO_IPV6POOL_NAT_OUTGOING: "true"
FELIX_IPV6SUPPORT: "true"
IP6: "autodetect"
CALICO_IPV6POOL_CIDR: "{{ spec.network.dualStack.IPv6podCIDR }}"`spec.network.kuberouter`
| Element | Description |
|---|---|
autoMTU |
Auto-detection of used MTU (default: true). |
mtu |
Override MTU setting, if autoMTU must be set to false. |
metricsPort |
Kube-router metrics server port. Set to 0 to disable metrics (default: 8080). |
peerRouterIPs |
DEPRECATED: Use extraArgs with peerRouterIPs instead. Comma-separated list of global peer addresses. |
peerRouterASNs |
DEPRECATED: Use extraArgs with peerRouterASNs instead. Comma-separated list of global peer ASNs. |
hairpin |
Configure hairpin mode. Supported modes are Enabled: enabled cluster wide, Allowed: must be allowed per service using annotations, Disabled: doesn't work at all (default: Enabled) |
hairpinMode |
Deprecated Use hairpin instead. If both hairpin and hairpinMode are defined, this is ignored. If only hairpinMode is configured explicitly activates hairpinMode. |
ipMasq |
IP masquerade for traffic originating from the pod network, and destined outside of it (default: false) |
extraArgs |
Extra arguments to pass to Kube-router. extraArgs are recommended over rawArgs if the use case allows it. Can be also used to override any k0s managed arguments. For reference, see Kube-router documentation. Any behavior triggered by these parameters is outside k0s support. (default: empty) |
rawArgs |
Slice of strings for any raw arguments to pass down to the kube-router process. These are appended after extraArgs. Any behavior triggered by these parameters is outside k0s support. (default: empty) |
Note: Kube-router allows many networking aspects to be configured per node, service, and pod (for more information, refer to the Kube-router user guide).
`spec.network.kubeProxy`
| Element | Description |
|---|---|
disabled |
Disable kube-proxy altogether (default: false). |
mode |
Kube proxy operating mode, supported modes iptables, ipvs, nftables¹, userspace (default: iptables) |
iptables |
Kube proxy iptables settings |
ipvs |
Kube proxy IPVS settings |
nftables |
Kube proxy nftables settings |
nodePortAddresses |
Kube proxy nodePortAddresses |
healthzBindAddress |
Kube proxy healthzBindAddress (default: 0.0.0.0:10256) |
metricsBindAddress |
Kube proxy metricsBindAddress (default: 0.0.0.0:10249) |
extraArgs |
Map of key-values (strings) for any extra arguments to pass down to kube-proxy process. extraArgs are recommended over rawArgs if the use case allows it. Any behavior triggered by these parameters is outside k0s support. (default: empty) |
rawArgs |
Slice of strings for any raw arguments to pass down to the kube-proxy process. These are appended after extraArgs. If possible, it's recommended to use extraArgs over rawArgs. Any behavior triggered by these parameters is outside k0s support. (default: empty) |
¹ For nftables, the kube-proxy container's nftables version needs to be less
than or equal to the host OS's in order to avoid segmentation faults. For
example, a host OS with nftables v1.1.1 requiresquay.io/k0sproject/kube-proxy:{# no auto-bumps-here #}{{{ "v1.33.2" }}} or
lower.
Default kube-proxy iptables settings:
iptables:
masqueradeAll: false
masqueradeBit: null
minSyncPeriod: 0s
syncPeriod: 0sDefault kube-proxy IPVS settings:
ipvs:
excludeCIDRs: null
minSyncPeriod: 0s
scheduler: ""
strictARP: false
syncPeriod: 0s
tcpFinTimeout: 0s
tcpTimeout: 0s
udpTimeout: 0sDefault kube-proxy nftables settings:
nftables:
masqueradeAll: false
masqueradeBit: null
minSyncPeriod: 0s
syncPeriod: 0s`spec.network.nodeLocalLoadBalancing`
Configuration options related to k0s's node-local load balancing feature.
| Element | Description |
|---|---|
enabled |
Indicates if node-local load balancing should be used to access Kubernetes API servers from worker nodes. Default: false. |
type |
The type of the node-local load balancer to deploy on worker nodes. Default: EnvoyProxy. Supported values: EnvoyProxy, Traefik. |
envoyProxy |
Configuration options related to the "EnvoyProxy" type of load balancing. |
traefik |
Configuration options related to the "Traefik" type of load balancing. |
`spec.network.nodeLocalLoadBalancing.envoyProxy`
Configuration options required for using Envoy as the backing implementation for
node-local load balancing.
Note: This type of load balancing is not supported on ARMv7, RISC-V and
Windows.
| Element | Description |
|---|---|
image |
The OCI image that's being used for the Envoy Pod. |
imagePullPolicy |
The pull policy being used used for the Envoy Pod. Defaults to spec.images.default_pull_policy if omitted. |
apiServerBindPort |
Port number on which to bind the Envoy load balancer for the Kubernetes API server to on a worker's loopback interface. Default: 7443. |
konnectivityServerBindPort |
Port number on which to bind the Envoy load balancer for the konnectivity server to on a worker's loopback interface. Default: 7132. |
`spec.network.nodeLocalLoadBalancing.traefik`
Configuration options required for using Traefik as the backing implementation
for node-local load balancing.
| Element | Description |
|---|---|
image |
The OCI image that's being used for the Traefik Pod. |
imagePullPolicy |
The pull policy being used used for the Traefik Pod. Defaults to spec.images.default_pull_policy if omitted. |
apiServerBindPort |
Port number on which to bind the Traefik load balancer for the Kubernetes API server to on a worker's loopback interface. Default: 7443. |
konnectivityServerBindPort |
Port number on which to bind the Traefik load balancer for the konnectivity server to on a worker's loopback interface. Default: 7132. |
`spec.network.controlPlaneLoadBalancing`
Configuration options related to k0s's control plane load balancing feature
| Element | Description |
|---|---|
enabled |
Indicates if control plane load balancing should be enabled. Default: false. |
type |
The type of the control plane load balancer to deploy on controller nodes. Currently, the only supported type is Keepalived. |
keepalived |
Contains the keepalived configuration. |
`spec.network.controlPlaneLoadBalancing.Keepalived`
Configuration options related to keepalived in control plane load balancing
| Element | Description |
|---|---|
vrrpInstances |
Configuration options related to the VRRP. This is an array which allows to configure multiple virtual IPs. |
virtualServers |
Configuration options related to load balancing. This is an array which allows to configure multiple load balancers. |
userSpaceProxyBindPort |
The port the user space proxy will bind to. This port is for internal use only, but listens on every interface. Default: 6444 |
disableLoadBalancer |
Disables the load balancer. Default: false |
configTemplateVRRP |
Path to a custom Keepalived configuration template for VRRP. Default: empty . |
configTemplateVS |
Path to a custom Keepalived configuration template for Virtual Servers. Default: empty |
`spec.network.controlPlaneLoadBalancing.keepalived.vrrpInstances`
Configuration options required for using VRRP to configure VIPs in control plane load balancing.
| Element | Description |
|---|---|
virtualIPs |
List of virtual IP address used by the VRRP instance. Each virtual IP must be a CIDR as defined in RFC 4632 and RFC 4291.A list of the CIDRs handled by the VRRP instance. |
interface |
The NIC used by the virtual router. K0s allows to use either a device name or a MAC address. If not specified, k0s will use the interface that owns the default route. |
virtualRouterID |
The VRRP router ID. If not specified, k0s will automatically number the IDs for each VRRP instance, starting with 51. It must be in the range of 1-255, all the control plane nodes must use the same virtualRouterID. Other clusters in the same network must not use the same virtualRouterID. |
advertIntervalSeconds |
Advertisement interval in seconds. Defaults to 1 second. |
authPass |
The password for accessing VRRPD. This is not a security feature but a way to prevent accidental misconfigurations. It must be in the range of 1-8 characters |
unicastPeers |
A list of IP addresses to connect using unicast. If this field is specified, unicastSourceIP is mandatory, and this list must not contain the IP address specified in unicastSourceIP. |
unicastSourceIP |
The source IP address when using unicast. If unicastPeers isn't defined this field is ignored. |
addressLabel |
The label for the VRRP instance for IPv6 VIPs. This value is ignored for IPv4 VIPs. This is used to set the routing preference as per RFC 6724. If set to 0, uses default value. Default: 10000 |
`spec.network.controlPlaneLoadBalancing.keepalived.virtualServers`
Configuration options required for using VRRP to configure VIPs in control plane load balancing.
| Element | Description |
|---|---|
ipAddress |
The load balancer's listen address. |
delayLoop |
Delay timer for check polling. Accepts microsecond precision. Further precision will be truncated without warnings. Defaults to 1m. |
lbAlgo |
Algorithm used by keepalived. Supported algorithms: rr, wrr, lc, wlc, lblc, dh, sh, sed, nq. Default: rr. |
lbKind |
Kind of IPVS load balancer. Supported values: NAT, DR, TUN. Default: DR. |
persistenceTimeoutSeconds |
Timeout for persistent connections in seconds. Must be in the range of 1-2678400 (31 days). If not specified, defaults to 360 (6 minutes). |
`spec.controllerManager`
| Element | Description |
|---|---|
extraArgs |
Map of key-values (strings) for any extra arguments you want to pass down to the Kubernetes controller manager process. extraArgs are recommended over rawArgs if the use case allows it. Any behavior triggered by these parameters is outside k0s support. (default: empty) |
rawArgs |
Slice of strings for any raw arguments to pass down to the kube-controller-manager process. These are appended after extraArgs. If possible, it's recommended to use extraArgs over rawArgs. Any behavior triggered by these parameters is outside k0s support. (default: empty) |
`spec.scheduler`
| Element | Description |
|---|---|
extraArgs |
Map of key-values (strings) for any extra arguments you want to pass down to Kubernetes scheduler process. extraArgs are recommended over rawArgs if the use case allows it. Any behavior triggered by these parameters is outside k0s support. (default: empty) |
rawArgs |
Slice of strings for any raw arguments to pass down to the kube-scheduler process. These are appended after extraArgs. If possible, it's recommended to use extraArgs over rawArgs. Any behavior triggered by these parameters is outside k0s support. (default: empty) |
`spec.workerProfiles`
Worker profiles are used to manage worker-specific configuration in a
centralized manner. A ConfigMap is generated for each worker profile. Based on
the --profile argument given to the k0s worker, the configuration in the
corresponding ConfigMap is picked up during start-up.
The worker profiles are defined as an array. Each element has following
properties:
| Property | Description |
|---|---|
name |
String; name to use as profile selector for the worker process |
values |
Object; Kubelet configuration overrides, see below for details |
`spec.workerProfiles[].values` (Kubelet configuration overrides)
The Kubelet configuration overrides of a profile override the defaults defined
by k0s.
Note that there are several fields that cannot be overridden:
clusterDNSclusterDomainapiVersionkindstaticPodURL
Configuration examples
Custom volumePluginDir
spec:
workerProfiles:
- name: custom-pluginDir
values:
volumePluginDir: /var/libexec/k0s/kubelet-plugins/volume/execEviction Policy
spec:
workerProfiles:
- name: custom-eviction
values:
evictionHard:
memory.available: "500Mi"
nodefs.available: "1Gi"
imagefs.available: "100Gi"
evictionMinimumReclaim:
memory.available: "0Mi"
nodefs.available: "500Mi"
imagefs.available: "2Gi"Unsafe Sysctls
spec:
workerProfiles:
- name: custom-eviction
values:
allowedUnsafeSysctls:
- fs.inotify.max_user_instances`spec.featureGates`
Available components are:
kube-apiserverkube-controller-managerkubeletkube-schedulerkube-proxy
If components is omitted, propagates to all Kubernetes components.
Modifies extraArgs.
Examples
Generic feature gates example
spec:
featureGates:
- name: feature-gate-0
enabled: true
components:
[
"kube-apiserver",
"kube-controller-manager",
"kubelet",
"kube-scheduler",
]
- name: feature-gate-1
enabled: true
- name: feature-gate-2
enabled: falseKubelet feature gates example
The below is an example of a k0s config with feature gates enabled:
spec:
featureGates:
- name: DevicePlugins
enabled: true
components: ["kubelet"]
- name: Accelerators
enabled: true
components: ["kubelet"]
- name: AllowExtTrafficLocalEndpoints
enabled: false`spec.images`
Nodes under the images key all have the same basic structure:
spec:
images:
coredns:
image: {{{ src_var('CoreDNSImage') }}}
version: {{{ src_var('CoreDNSImageVersion') }}}If you want the list of default images and their versions to be included, use k0s config create --include-images.
Available keys
spec.images.konnectivityspec.images.metricsserverspec.images.kubeproxyspec.images.corednsspec.images.pausespec.images.calico.cnispec.images.calico.nodespec.images.calico.kubecontrollersspec.images.kuberouter.cnispec.images.kuberouter.cniInstallerspec.images.repository¹spec.images.default_pull_policy²
¹ If spec.images.repository is set and not empty, every image will be pulled from there.
² If spec.images.default_pull_policy is set and not empty, it will be used as a pull policy for each bundled image.
Image example
{% set cali_ver = src_var('CalicoKubeControllersImageVersion') -%}
{% set metrics_ver = src_var('MetricsImageVersion') -%}
images:
repository: airgap-repo.local
calico:
kubecontrollers:
image: repo.acme.corp/k0sproject/calico-kube-controllers
version: {{{ cali_ver }}}
metricsserver:
image: repo.acme.corp/k0sproject/metrics-server
version: {{{ metrics_ver }}}In the runtime the image names are calculated asairgap-repo.local/k0sproject/calico-kube-controllers:{{{ cali_ver }}} andairgap-repo.local/k0sproject/metrics-server:{{{ metrics_ver }}}. This only
affects the image pull location, and thus omitting an image specification here
will not disable component deployment.
`spec.extensions.helm`
spec.extensions.helm is the config file key in which you configure the list of Helm repositories and charts to deploy during cluster bootstrap (for more information, refer to Helm Charts).
`spec.konnectivity`
The spec.konnectivity key is the config file key in which you configure Konnectivity-related settings.
agentPortagent port to listen on (default 8132)adminPortadmin port to listen on (default 8133)
`spec.telemetry`
To improve the end-user experience k0s is configured by default to collect telemetry data from clusters and send it to the k0s development team. To disable the telemetry function, change the enabled setting to false.
The telemetry interval is ten minutes.
spec:
telemetry:
enabled: trueComponent patches
!!! warning "Experimental feature"
Patching is an experimental feature, which means the behavior of this feature might be unstable and may change without a major version bump.
Several k0s-managed components let you customize the Kubernetes resources they
generate before those resources are written and applied. This is an escape
hatch for adjustments k0s does not expose as dedicated config options. Patches
live under the component they belong to:
| Component | Config key |
|---|---|
| CoreDNS | spec.network.coreDNS.patches |
| kube-proxy | spec.network.kubeProxy.patches |
| kube-router | spec.network.kuberouter.patches |
| Calico | spec.network.calico.patches |
| metrics-server | spec.metricsServer.patches |
Each entry selects a target resource by kind and name (optionally narrowed
by namespace) and provides the patch type and content. The content may
be written as JSON or YAML.
| Element | Description |
|---|---|
target.kind |
The Kubernetes kind of the generated resource to patch (e.g. Deployment, Service, ConfigMap). |
target.name |
The metadata.name of the generated resource to patch. |
target.namespace |
Optional. Narrows the match to a single namespace. |
patch.type |
One of JSON (RFC 6902 JSON Patch), MergePatch (RFC 7386 JSON Merge Patch) or StrategicMergePatch. |
patch.content |
The patch body, as JSON or YAML. |
Multiple patches matching the same resource are applied in the order listed.
Note that StrategicMergePatch patches are only supported for built-in
Kubernetes resource kinds.
Example
spec:
network:
coreDNS:
patches:
- target:
kind: Deployment
name: coredns
namespace: kube-system
patch:
type: StrategicMergePatch
content: |
spec:
replicas: 3
template:
spec:
containers:
- name: coredns
resources:
limits:
memory: 256MiDisabling controller components
k0s allows to completely disable some of the system components. This allows
users to build a minimal Kubernetes control plane and use what ever components
they need to fulfill their need for the control plane. Disabling the system
components happens through a command line flag for the controller process:
--disable-components strings disable components (valid items: applier-manager,autopilot,control-api,coredns,csr-approver,endpoint-reconciler,helm,konnectivity-server,kube-controller-manager,kube-proxy,kube-scheduler,metrics-server,network-provider,node-role,system-rbac,update-prober,windows-node)If you use k0sctl, just add the flag when installing the cluster for the first
controller at spec.hosts.installFlags in the config file like e.g.:
spec:
hosts:
- role: controller
installFlags:
- --disable-components=metrics-serverAs seen from the component list, the only always-on component is the Kubernetes
API server, without that k0s serves no purpose.
Kubelet root directory
Unlike vanilla Kubernetes, k0s by default deploys kubelet's root directory
inside --data-dir, which defaults to /var/lib/k0s/ and kubelet ultimately
runs in /var/lib/k0s/kubelet. This can result in incompatibilities on external
software that mounts the kubelet working directory, such as CSI plugins.
This can be changed using the flag --kubelet-root-dir, for instance, to use
Kubernetes' default value it can be set up as:
sudo k0s install worker --token-file /var/lib/k0s/join-token --kubelet-root-dir=/var/lib/kubeletKeep in mind that changing the flag in an existing node will not remove existing directories.
Equally importantly, some pods such as CSI plugins are likely to mount this directory,
having inconsistent values across nodes is very likely to cause problems on Deployments and
DaemonSets.
Cplb (`docs/cplb.md`)
Control plane load balancing
For clusters that don't have an externally managed load balancer for the k0s
control plane, there is another option to get a highly available control plane called control plane load balancing (CPLB).
CPLB provides clusters a highly available VIP (virtual IP) and load balancing for accessing the cluster externally.
For internal traffic (nodes to control plane) k0s provides NLLB. Both features are fully compatible and it's
recommended to use both together if you don't have an external load balancer.
Load balancing means that an IP address will forward the traffic to every control plane node, Virtual IPs mean that
this IP address will be present on at least one node at a time.
CPLB relies on keepalived for highly available VIPs. Internally, Keepalived uses the
VRRP protocol. Load Balancing can be done through either userspace
reverse proxy implemented in k0s (recommended for simplicity), or it can use Keepalived's virtual servers feature,
which ultimately relies on IPVS.
Compatibility
CPLB depends on multiple technologies to work together as a whole, making it difficult to work on every single scenario.
Single node
CPLB is incompatible with running as a single node. This means k0s must not be started using the --single flag.
Controller + worker
K0s only supports the userspace reverse proxy load balancer. Keepalived's VirtualServers are not supported with controller + worker.
Both Kube-Router and Calico managed by k0s are supported with the userspace reverse proxy load balancer, however, k0s creates iptables
rules in the control plane nodes which may be incompatible with custom CNI plugins.
External address
If spec.api.externalAddress is defined, control
plane load balancing implicitly
disables k0s's endpoint
reconciler component, just as if the --disable-components=endpoint-reconciler
flag had been specified.
Node Local load balancing
CPLB is fully compatible with NLLB, however NLLB is incompatible with spec.api.externalAddress.
Virtual IPs - High availability
What is a VIP (virtual IP)
A virtual IP is an IP address that isn't tied to a single network interface,
instead it floats between multiple servers. This is a failover mechanism that
grants that there is always at least a functioning server and removes a single
point of failure.
Configuring VIPs
CPLB relies internally on Keepalived's VRRP Instances. A VRRP Instance is a
server that will manage one or more VIPs. Most users will need exactly one
VRRP instance with exactly one VIP, however k0s allows multiple VRRP servers
with multiple VIPs for more advanced use cases such as network segmentation.
A virtualIP requires:
- A user-defined CIDR address which must be routable in the network. For most installations, this will be in the same CIDR as the physical interface.
WARNING: K0s is not aware of external IP address management and the administrator is responsible for ensuring that IP addresses aren't colliding. - A user-defined password which should be unique for each cluster. This password is a mechanism to prevent accidental conflicts. It's not encrypted
and doesn't prevent malicious attacks in any way. - A virtual router ID, which defaults to 51. This virtual router ID must be unique in the broadcast domain.
- A network interface, if not defined, k0s will chose the network interface that owns the default route.
Except the network interface, all the other fields must be equal on every control plane node.
This is a minimal example:
spec:
network:
controlPlaneLoadBalancing:
enabled: true
type: Keepalived
keepalived:
vrrpInstances:
- virtualIPs: ["<VIP address>/<netmask>"] # for instance ["172.16.0.100/16"]
authPass: "<my password>"By default, VRRP Instances use multicast as per RFC 3768. It's possible to configure VRRP
instances to use unicast:
spec:
network:
controlPlaneLoadBalancing:
enabled: true
type: Keepalived
keepalived:
vrrpInstances:
- virtualIPs: ["<VIP address>/<netmask>"] # for instance ["172.16.0.100/16"]
authPass: "<my password>"
unicastSourceIP: <ip address of this controller>
unicastPeers: [<ip address of other controllers>, ...]When using unicast, k0st does not attempt to detect unicastSourceIP and it must be defined explicitly andunicastPeers must include the IP address of the other controllers' unicastSourceIP.
IPv6 VIPs egress routing preference
In IPv6 there aren't primary addresses, instead the routing preference is determined by the operating system using
either IP labels or IP rules. K0s Virtual Addresses uses RFC 6724 IP labels. By default, k0s sets the label to
10000 so that it still uses the main IP address as a source of egress connections.
This value may be replaced per vrrPInstance:
spec:
network:
controlPlaneLoadBalancing:
enabled: true
type: Keepalived
keepalived:
vrrpInstances:
- virtualIPs: ["<VIP address>/<netmask>"] # for instance ["2001:db8:2::1/64"]
authPass: "<my password>"
addressLabel: 30000The label value can be verified using iproute2:
$ ip addrlabel | grep 2001:db8:2::1
prefix 2001:db8:2::1/128 label 30000The prefix always uses netmask 128.
K0s doesn't attempt to modify labels that do not belong to the VIP.
Load Balancing
Currently k0s allows to chose one of two load balancing mechanism:
- A userspace reverse proxy running in the k0s process. This is the default and recommended setting.
- For users who may need extra performance or more flexible algorithms, k0s can use the keepalived virtual servers load balancer feature.
All control plane nodes must use the same load balancing mechanism. Different Load balancing mechanism
is not supported and has undefined behavior.
Load Balancing - Userspace Reverse Proxy
This is the default behavior, in order to enable it simple configure a VIP
using a VRRP instance.
spec:
network:
controlPlaneLoadBalancing:
enabled: true
type: Keepalived
keepalived:
vrrpInstances:
- virtualIPs: ["<VIP address>/<netmask>"] # for instance ["172.16.0.100/16"]
authPass: "<my password>"Keepalived Virtual Servers Load Balancing
The Keepalived virtual servers Load Balancing is more performant than the userspace reverse proxy load balancer. However, it's
not recommended because it has some drawbacks:
- It's incompatible with controller+worker.
- May not work on every infrastructure.
- Troubleshooting is significantly more complex.
- When there is more than one VRRPInstance, we must do the load balancing in all the servers
which in some rare circumstances can provoke temporary routing loops.
spec:
network:
controlPlaneLoadBalancing:
enabled: true
type: Keepalived
keepalived:
vrrpInstances:
- virtualIPs: ["<VIP address>/<netmask>"] # for instance ["172.16.0.100/16"]
authPass: "<my password>"
virtualServers:
- ipAddress: "<VIP address without netmask>" # for instance 172.16.0.100Full example using `k0sctl`
The following example shows a full k0sctl configuration file featuring three
controllers and three workers with control plane load balancing enabled.
/* Detailed source-code truncated for AI context efficiency. */Save the above configuration into a file called k0sctl.yaml and apply it in
order to bootstrap the cluster:
/* Detailed source-code truncated for AI context efficiency. */The cluster with the two nodes should be available by now. Setup the kubeconfig
file in order to interact with it:
k0sctl kubeconfig > k0s-kubeconfig
export KUBECONFIG=$(pwd)/k0s-kubeconfigAll three worker nodes are ready:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
worker-0.k0s.lab Ready <none> 8m51s {{{ k8s_version }}}+k0s
worker-1.k0s.lab Ready <none> 8m51s {{{ k8s_version }}}+k0s
worker-2.k0s.lab Ready <none> 8m51s {{{ k8s_version }}}+k0sOnly one controller has the VIP:
$ for i in controller-{0..2} ; do echo $i ; ssh $i -- ip -4 --oneline addr show | grep eth0; done
controller-0
2: eth0 inet 192.168.122.37/24 brd 192.168.122.255 scope global dynamic noprefixroute eth0\ valid_lft 2381sec preferred_lft 2381sec
2: eth0 inet 192.168.122.200/24 scope global secondary eth0\ valid_lft forever preferred_lft forever
controller-1
2: eth0 inet 192.168.122.185/24 brd 192.168.122.255 scope global dynamic noprefixroute eth0\ valid_lft 2390sec preferred_lft 2390sec
controller-2
2: eth0 inet 192.168.122.87/24 brd 192.168.122.255 scope global dynamic noprefixroute eth0\ valid_lft 2399sec preferred_lft 2399secThe cluster is using control plane load balancing and is able to tolerate the
outage of one controller node. Shutdown the first controller to simulate a
failure condition:
$ ssh controller-0 'sudo poweroff'
Connection to 192.168.122.37 closed by remote host.Control plane load balancing provides high availability, the VIP will have moved to a different node:
$ for i in controller-{1..2} ; do echo $i ; ssh $i -- ip -4 --oneline addr show | grep eth0; done
controller-1
2: eth0 inet 192.168.122.185/24 brd 192.168.122.255 scope global dynamic noprefixroute eth0\ valid_lft 2173sec preferred_lft 2173sec
2: eth0 inet 192.168.122.200/24 scope global secondary eth0\ valid_lft forever preferred_lft forever
controller-2
2: eth0 inet 192.168.122.87/24 brd 192.168.122.255 scope global dynamic noprefixroute eth0\ valid_lft 2182sec preferred_lft 2182secAnd the cluster will be working normally:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
worker-0.k0s.lab Ready <none> 8m51s {{{ k8s_version }}}+k0s
worker-1.k0s.lab Ready <none> 8m51s {{{ k8s_version }}}+k0s
worker-2.k0s.lab Ready <none> 8m51s {{{ k8s_version }}}+k0sTroubleshooting
Although Virtual IP addresses and load balancing work together and are closely
related, these are two independent processes and must be troubleshooting as two
independent features.
Troubleshooting Virtual IPs
The first thing to check is that the VIP is present in exactly one node at a time,
for instance if a cluster has an 172.17.0.102/16 address and the interface is eth0,
the expected output is similar to:
controller0:/# ip a s eth0
53: eth0@if54: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet 172.17.0.102/16 scope global secondary eth0
valid_lft forever preferred_lft forevercontroller1:/# ip a s eth0
55: eth0@if56: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft foreverIf the virtualServers feature is used, there must be a dummy interface on the
node called dummyvip0 which has the VIP, but with a /32 bits network mask.
This isn't the VIP and has to be there even if the VIP is held by another node.
controller0:/# ip a s dummyvip0 | grep 172.17.0.102
inet 172.17.0.102/32 scope global dummyvip0controller1:/# ip a s dummyvip0 | grep 172.17.0.102
inet 172.17.0.102/32 scope global dummyvip0If this isn't present in the nodes, keepalived logs can be seen in the k0s-logs, and
can be filtered with component=keepalived.
controller0:/# journalctl -u k0scontroller | grep component=keepalived
time="2024-11-19 12:56:11" level=info msg="Starting to supervise" component=keepalived
time="2024-11-19 12:56:11" level=info msg="Started successfully, go nuts pid 409" component=keepalived
time="2024-11-19 12:56:11" level=info msg="Tue Nov 19 12:56:11 2024: Starting Keepalived v{{{ build_var('keepalived_version') }}}" component=keepalived stream=stderr
[...]The Keepalived configuration is stored in a file called keepalived.conf in the
k0s run directory, by default /run/k0s/keepalived.conf, in this file there
should be a vrrp_instance section for each vrrpInstance.
Finally, k0s should have two keepalived processes running.
Troubleshooting the Load Balancer's Endpoint List
Both the user space reverse proxy load balancer and Keepalived's virtual servers need an endpoint list to
do the load balancing. They share a component called cplb-reconciler which responsible for setting the
load balancer's endpoint list. This component monitors constantly the endpoint kubernetes in thedefaultnamespace:
controller0:/# kubectl get ep kubernetes -n default
NAME ENDPOINTS AGE
kubernetes 172.17.0.6:6443,172.17.0.7:6443,172.17.0.8:6443 9m14sYou can see the cplb-reconciler updates by running:
controller0:/# journalctl -u k0scontroller | grep component=cplb-reconciler
time="2024-11-20 20:29:28" level=error msg="Failed to watch API server endpoints, last observed version is \"\", starting over in 10s ..." component=cplb-reconciler error="Get \"https://172.17.0.6:6443/api/v1/namespaces/default/endpoints?fieldSelector=metadata.name%3Dkubernetes&timeout=30s&timeoutSeconds=30\": dial tcp 172.17.0.6:6443: connect: connection refused"
time="2024-11-20 20:29:38" level=info msg="Updated the list of IPs: [172.17.0.6]" component=cplb-reconciler
time="2024-11-20 20:29:55" level=info msg="Updated the list of IPs: [172.17.0.6 172.17.0.7]" component=cplb-reconciler
time="2024-11-20 20:29:59" level=info msg="Updated the list of IPs: [172.17.0.6 172.17.0.7 172.17.0.8]" component=cplb-reconcilerTroubleshooting the user space reverse proxy load balancer
The user space reverse proxy load balancer runs in the k0s process. It listens a separate socket, by default on port 6444:
controller0:/# netstat -tlpn | grep 6444
tcp 0 0 :::6444 :::* LISTEN 345/k0sThen the requests to the VIP on the apiserver port are forwarded to this socket using one iptables rule:
-A PREROUTING -d <VIP>/32 -p tcp -m tcp --dport <apiserver port> -j REDIRECT --to-ports <userspace proxy port>A real life example of a cluster using the VIP 17.177.0.102 looks like:
controller0:/# /var/lib/k0s/bin/iptables-save | grep 6444
-A PREROUTING -d 172.17.0.102/32 -p tcp -m tcp --dport 6443 -j REDIRECT --to-ports 6444Keep in mind that clusters using IPv6 as a primary address family, should use ip6tables or ip6tables-save.
It the load balancer is not load balancing for whatever reason, you can establish connections to it directly. A good way to see if it's actually load balancing is checking the serving certificate:
controller0:/# ip -o addr s eth0
43: eth0 inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0\ valid_lft forever preferred_lft forever
43: eth0 inet 172.17.0.102/16 scope global secondary eth0\ valid_lft forever preferred_lft forever
controller0:/# openssl s_client -connect 172.17.0.102:6444 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint
SHA1 Fingerprint=B7:90:E6:E4:E1:EE:5B:19:72:99:02:28:54:36:D9:84:D5:39:67:8B
controller0:/# openssl s_client -connect 172.17.0.102:6444 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint
SHA1 Fingerprint=89:94:5C:E5:50:7E:40:B2:E5:20:E7:70:E8:58:91:ED:63:B0:EC:65
controller0:/# openssl s_client -connect 172.17.0.102:6444 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint
SHA1 Fingerprint=49:0D:79:FD:79:6F:A0:E4:9D:BA:A1:65:9C:C5:54:CF:E5:20:BF:A8
controller0:/# openssl s_client -connect 172.17.0.102:6444 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint
SHA1 Fingerprint=B7:90:E6:E4:E1:EE:5B:19:72:99:02:28:54:36:D9:84:D5:39:67:8BNote: You can't query the port 6444 on the localhost address, there is an iptables conflict. You are expected to
be able to reach the port 6443 on any address and the port 6444 on any address except localhost.
Troubleshooting Keepalived Virtual Servers
You can verify the Keepalived logs and configuration file using the steps described in the section
troubleshooting virtual IPs above.
When virtual servers are enabled K0s generates two additional files:
keepalived-virtualservers-generated.conf: This file contains the list of control plane nodes that should be balanced to.keepalived-virtualservers-consumed.conf: This is a symbolic link which points tokeepalived-virtualservers-generated.conf
if the Keepalived VRRP instance's current state ismasteror to/dev/nullif it'sbackup. This file is only generated if
there is exactly one VRRP instance.
Additionally, you can check the actual IPVS configuration using ipvsadm:
controller0:/# ipvsadm --save -n
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 192.168.122.200:6443 rr persistent 360
-> 192.168.122.185:6443 Route 1 0 0
-> 192.168.122.87:6443 Route 1 0 0
-> 192.168.122.122:6443 Route 1 0 0 In this example 192.168.122.200 is the virtual IP, and 192.168.122.185, 192.168.122.87
and 192.168.122.122 are the control plane nodes.
If there is only one VRRP instance, only the current master should be load balancing.
Custom Keepalived Templates
Warning: Any customization to Keepalived templates is outside the scope of k0s support.
Template variables are not considered a stable API and templates can break during upgrades. Use at your own risk.
K0s allows you to customize the Keepalived configuration by providing custom Go templates for advanced users
who need an extra layer of customization. A minimal example would be:
spec:
network:
controlPlaneLoadBalancing:
enabled: true
type: Keepalived
keepalived:
vrrpInstances:
- virtualIPs: ["<VIP address>/<netmask>"]
authPass: "<my password>"
configTemplateVRRP: /path/to/custom-vrrp-template.conf
configTemplateVS: /path/to/custom-virtualservers-template.confIt is recommended to create custom templates based on the default templates, these can be accessed using the following commands:
# View the default VRRP template
k0s keepalived-config vrrp
# View the default Virtual Servers template
k0s keepalived-config virtualserversThe templates are only read during the k0s bootstrap. If a template is changed it's required to restart k0s.
Keepalived template variables
VRRP Variables (configTemplateVRRP)
| Variable | Description |
|---|---|
.IPVSLoadBalancer |
Determines whether IPVS load balancer is enabled or not. |
.K0sBin |
K0s binary path. |
.RunDir |
K0s run directory. |
.VRRPInstances |
Struct holding all the VRRP instances. |
.VRRPInstances[].AdvertIntervalSeconds |
Advert interval of the VRRP instance. |
.VRRPInstances[].AuthPass |
Password for accessing VRRPD. |
.VRRPInstances[].Interface |
Network interface used by the virtual router. |
.VRRPInstances[].UnicastPeers |
List of unicast peer IP addresses. |
.VRRPInstances[].UnicastSourceIP |
Source IP address for unicast communication. |
.VRRPInstances[].VirtualIPs |
List of virtual IP addresses with CIDR notation. |
.VRRPInstances[].VirtualRouterID |
VRRP router ID. |
Virtual Servers Variables (configTemplateVS)
| Variable | Description |
|---|---|
.APIServerPort |
Kubernetes API server port. |
.RealServers |
List of real server IP addresses (control plane nodes). |
.VirtualServers |
Array of virtual server configurations. |
.VirtualServers[].DelayLoop |
Delay timer for check polling. |
.VirtualServers[].IPAddress |
Virtual IP address used by the virtual server. |
.VirtualServers[].LBAlgo |
Load balancing algorithm. |
.VirtualServers[].LBKind |
Load balancing forwarding method. |
.VirtualServers[].PersistenceTimeoutSeconds |
Timeout value for persistent connections in seconds. |
Important Considerations
- No Support: Custom template configurations are not officially supported. If you encounter issues with custom templates, you may be asked to reproduce the issue with default templates.
- Updates: When upgrading k0s, review the default templates for any changes that may need to be incorporated into your custom templates.
- Testing: Always test custom templates thoroughly in a non-production environment before deploying to production.
- Syntax Validation: k0s performs minimal validation of custom templates. Invalid Keepalived configuration will cause the CPLB feature to fail.
Custom Ca (`docs/custom-ca.md`)
Install using custom CA certificates and SA key pair
k0s generates all needed certificates automatically in the <data-dir>/pki directory (/var/lib/k0s/pki, by default).
But sometimes there is a need to have the CA certificates and SA key pair in advance.
To make it work, just put files to the <data-dir>/pki and <data-dir>/pki/etcd:
export LIFETIME=365
mkdir -p /var/lib/k0s/pki/etcd
cd /var/lib/k0s/pki
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days $LIFETIME -out ca.crt -subj "/CN=Custom CA"
openssl genrsa -out sa.key 2048
openssl rsa -in sa.key -outform PEM -pubout -out sa.pub
cd ./etcd
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days $LIFETIME -out ca.crt -subj "/CN=Custom CA"Then you can install k0s as usual.
Pre-generated tokens
It's possible to get join in advance without having a running cluster.
k0s token pre-shared --role worker --cert /var/lib/k0s/pki/ca.crt --url https://<controller-ip>:6443/The command above generates a join token and a Secret. A Secret should be deployed to the cluster to authorize the token.
For example, you can put the Secret under the manifest directory and it will be deployed automatically.
Please note that if you are generating a join token for a controller, the port number needs to be 9443 instead of 6443.
Controller bootstrapping requires talking to the k0s API server instead of the Kubernetes API server.
Here's an example of a command for pre-generating a token for a controller.
k0s token pre-shared --role controller --cert /var/lib/k0s/pki/ca.crt --url https://<controller-ip>:9443/See also
Directories (`docs/directories.md`)
k0s directories
This page describes the directories k0s reads and writes on the host, what they
are used for, and the permissions k0s expects on them.
Please note that the directory layouts described here are considered an
implementation detail and may change without further notice, even between patch
releases, unless explicitly stated otherwise, e.g., by other parts of the
documentation referencing those paths or files within them.
The two parent directories are configurable via CLI flags:
--data-dir(default/var/lib/k0son Linux,C:\var\lib\k0son Windows)
persistent state--run-dir(default/run/k0son Linux)
runtime state such as PID files and unix sockets
Additionally, on k0s nodes with workloads enabled, you can set the following directories:
--kubelet-root-dir(default<data-dir>/kubelet)
Kubelet root directory
Data directory layout
Inside <data-dir> (default /var/lib/k0s):
| Path | Purpose | Mode |
|---|---|---|
<data-dir>/ |
Top-level data directory. | 0755 |
<data-dir>/bin/ |
Embedded binaries (kubelet, etcd, containerd, …) extracted on first start. | 0755 |
<data-dir>/pki/ |
Cluster CA and component certificates managed by k0s. | 0751 |
<data-dir>/pki/etcd/ |
etcd-specific certificates. | 0711 |
<data-dir>/etcd/ |
etcd database files if k0s is configured to use the embedded etcd. | 0700 |
<data-dir>/manifests/ |
Manifest Deployer drop-in directory. Files placed here are applied to the cluster. | 0755 |
<data-dir>/images/ |
Image bundles imported into the managed containerd instance | 0755 |
<data-dir>/kine/ |
Kine database files when k0s is configured to use Kine instead of etcd. | 0750 |
Individual certificate files inside pki/ are written with mode 0644
(public certs) or 0640 (private keys), and admin/kubelet config files are
written with mode 0600.
Run directory layout
Inside <run-dir> (default /run/k0s):
| Path | Purpose | Mode |
|---|---|---|
<run-dir>/ |
Top-level run directory. | 0755 |
<run-dir>/*.pid |
PID files for supervised processes (kubelet, etcd, containerd, …). | 0644 |
<run-dir>/status.sock |
Unix socket exposing local status to k0s status and similar commands. |
(process umask) |
The run directory is intentionally outside the data directory so that on
systems where /run is a tmpfs, runtime state is cleared on reboot while
persistent state under <data-dir> survives.
Notes
- All paths above are derived from the same
--data-dir/--run-dirflags
passed tok0sand propagated to its supervised components, so changing
the flags moves the whole tree consistently. - The CSI guide in the Storage section documents the kubelet root
path caveat, since the default differs from upstream Kubernetes
(<data-dir>/kubeletrather than/var/lib/kubelet). - Backup/restore covers
pki/,manifests/, andimages/, along with the etcd
or k0s database files, ik k0s is configured to use them. See
Backup/Restore for details.
Dual Stack (`docs/dual-stack.md`)
IPv4/IPv6 dual-stack networking
Enabling dual-stack networking in k0s allows your cluster to handle both IPv4 and
IPv6 addresses. Follow the configuration examples below to set up dual-stack mode.
Enabling dual-stack using the default CNI (Kube-router)
In order to enable dual-stack networking using the default CNI provider, use the
following example configuration:
spec:
network:
# Kube-router is the default CNI provider
# provider: kube-router
podCIDR: 10.244.0.0/16
serviceCIDR: 10.96.0.0/12
dualStack:
enabled: true
IPv6podCIDR: fd00::/108
IPv6serviceCIDR: fd01::/108This configuration will set up all Kubernetes components and Kube-router
accordingly for dual-stack networking.
Configuring the node CIDR mask size
By default, k0s uses a /117 node CIDR mask size for IPv6, which provides 2048
IP addresses per node and a /24 for IPv4 which provides 256 addresses per node.
For IPv6, using the example configuration IPv6PodCIDR: fd00::/108, there
are 9 bits available for node allocation (117 - 108 = 9) and 11 bits available for
pod allocation (128 - 117 = 11). This allows for 512 nodes per cluster and 2048
IPs per node.
For IPv4, using the default PodCIDR: 10.244.0.0/16, there are 8 bits available
for node allocation and 8 bits available for pod allocation. This allows for 256
nodes per cluster and 256 IPs per node.
per cluster and 256 IPs per node.
You can customize the node CIDR mask size using the controller manager's extra arguments:
spec:
controllerManager:
extraArgs:
node-cidr-mask-size-ipv6: "120"
node-cidr-mask-size-ipv4: "21"
network:
dualStack:
enabled: true
IPv6podCIDR: fd00::/108
IPv6serviceCIDR: fd01::/108Using Calico as the CNI provider
Calico does not support IPv6 tunneling in the default vxlan mode, so if you
prefer to use Calico as your CNI provider, make sure to select bird mode. Use
the following example configuration:
spec:
network:
provider: calico
calico:
mode: bird
podCIDR: 10.244.0.0/16
serviceCIDR: 10.96.0.0/12
dualStack:
enabled: true
IPv6podCIDR: fd00::/108
IPv6serviceCIDR: fd01::/108Specifying the default IP family
In Kubernetes dual stack clusters, by default all the services are single stack,
including kubernetes.default.svc, which is used to communicate with the
Kubernetes API servers.
This is specially important when specifying explicitlyspec.api.externalAddress or spec.api.address.
To explicitly define the family which will be used by default use the following
configuration:
spec:
network:
# primaryAddressFamily is optional
primaryAddressFamily: <IPv4|IPv6>If not defined explicitly, k0s will determine it based on spec.api.externalAddress,
if this field is not defined, k0s will use spec.api.address. If the field used is
a host name or both are empty, k0s will use IPv4.
Custom CNI providers
While the dual-stack configuration section configures all components managed by
k0s for dual-stack operation, the custom CNI provider must also be configured
accordingly. Refer to the documentation for your specific CNI provider to ensure
a proper dual-stack setup that matches that of k0s.
Additional Resources
For more detailed information and troubleshooting, refer to the following resources:
- https://kubernetes.io/docs/concepts/services-networking/dual-stack/
- https://kubernetes.io/docs/tasks/network/validate-dual-stack/
- https://www.tigera.io/blog/dual-stack-operation-with-calico-on-kubernetes/
- https://docs.tigera.io/calico/3.27/networking/ipam/ipv6#enable-dual-stack
Dynamic Configuration (`docs/dynamic-configuration.md`)
Dynamic configuration
k0s comes with the option to enable dynamic configuration for cluster level components. This covers all the components other than etcd (or SQLite) and the Kubernetes API server. This option enables k0s configuration directly via Kubernetes API as opposed to using a configuration file for all cluster configuration.
This feature has to be enabled on every controller in the cluster using the --enable-dynamic-config flag in k0s controller or k0s install controller commands. Mixing controllers with and without dynamic configuration enabled in the same cluster will cause a conflict.
Dynamic vs. static configuration
The existing and enabled-by-default method is what we call static configuration. That's the way where the k0s process reads the config from the given YAML file (or uses the default config if no config is given by user) and configures every component accordingly. This means that for any configuration change the cluster admin has to restart all controllers on the cluster and have matching configs on each controller node.
In dynamic configuration mode, the first controller to start when the cluster is created uses the provided YAML as the bootstrap configuration and stores it in the Kubernetes API. All other controllers retrieve the existing configuration from the Kubernetes API and use it to configure all components except etcd and the Kubernetes API server. After the initial cluster bootstrap, the configuration object in the Kubernetes API becomes the source of truth for all controllers.
Cluster configuration vs. controller node configuration
In the k0s configuration options there are some options that are cluster-wide and some that are specific to each controller node in the cluster. The following list outlines which options are controller node specific and have to be configured only via the local file:
spec.api- these options configure how the local Kubernetes API server is setupspec.storage- these options configure how the local storage (etcd or SQLite) is setupspec.network.controlPlaneLoadBalancing- these options configure how to setup Control Plane Load Balancing.
In case of an HA control plane, all the controllers will need this part of the configuration as otherwise they will not be able to get the storage and Kubernetes API server running.
Configuration location
The cluster wide configuration is stored in the Kubernetes API as a custom resource called clusterconfig. There's currently only one instance named k0s. You can edit the configuration with what ever means possible, for example with:
k0s config editThis will open the configuration object for editing in your system's default editor.
Configuration reconciliation
The dynamic configuration uses the typical operator pattern for operation. k0s controller will detect when the object changes and will reconcile the configuration changes to be reflected to how different components are configured. So say you want to change the MTU setting for Kube-router CNI networking you'd change the config to contain e.g.:
kuberouter:
mtu: 1350
autoMTU: falseThis will change the Kube-router related ConfigMap and thus make Kube-router to use different MTU settings for new pods.
Configuration options
The configuration object is a 1-to-1 mapping with the existing configuration YAML. All the configuration options EXCEPT options under spec.api and spec.storage are dynamically reconciled.
As with any Kubernetes cluster there are certain things that just cannot be changed on-the-fly, this is the list of non-changeable options:
network.podCIDRnetwork.serviceCIDRnetwork.providernetwork.controlPlaneLoadBalancingnetwork.primaryAddressFamily
During the manual installation of control plane nodes with k0s install, all these
non-changeable options must be defined in the configuration file. This is necessary
because these fields can be used before the dynamic configuration reconciler is
initialized. Both k0sctl and k0smotron handle this without user intervention.
Configuration status
The dynamic configuration reconciler operator will write status events for all the changes it detects. To see all dynamic config related events, use:
k0s config statusLAST SEEN TYPE REASON OBJECT MESSAGE
64s Warning FailedReconciling clusterconfig/k0s failed to validate config: [invalid pod CIDR invalid ip address]
59s Normal SuccessfulReconcile clusterconfig/k0s Successfully reconciler cluster config
69s Warning FailedReconciling clusterconfig/k0s cannot change CNI provider from kuberouter to calicoEnvironment Variables (`docs/environment-variables.md`)
Environment variables
k0s install does not support environment variables.
Setting environment variables for components used by k0s depends on the used init system. The environment variables set in k0scontroller or k0sworker service will be inherited by k0s components, such as etcd, containerd, konnectivity, etc.
Component specific environment variables can be set in k0scontroller or k0sworker service. For example: for CONTAINERD_HTTPS_PROXY, the prefix CONTAINERD_ will be stripped and converted to HTTPS_PROXY in the containerd process.
For those components having environment prefix convention such as ETCD_xxx,
they are handled specially, i.e. the prefix will not be stripped. For example,ETCD_MAX_WALS will still be ETCD_MAX_WALS in etcd process.
The proxy environment variables HTTP_PROXY, HTTPS_PROXY, NO_PROXY are
always overridden by component specific environment variables, soETCD_HTTPS_PROXY will still be converted to HTTPS_PROXY in etcd process.
systemd
Create a drop-in directory and add config file with a desired environment variable:
mkdir -p /etc/systemd/system/k0scontroller.service.d
tee -a /etc/systemd/system/k0scontroller.service.d/http-proxy.conf <<EOT
[Service]
Environment=HTTP_PROXY=192.168.33.10:3128
EOTOpenRC
Export the desired environment variable overriding service configuration in the/etc/conf.d directory:
echo 'export HTTP_PROXY="192.168.33.10:3128"' > /etc/conf.d/k0scontrollerExperimental Windows (`docs/experimental-windows.md`)
Run k0s worker nodes in Windows
IMPORTANT: Windows support for k0s is under active development and must be considered experimental.
Prerequisites
- The cluster must be running at least one worker node and control plane on Linux. You can use Windows to run additional worker nodes. Supported Windows versions: Windows Server 2019 and Windows Server 2022.
Installation
1. Generate worker token from control plane
On your Linux control plane node, generate a worker token:
k0s token create --role=worker > worker-token.txtTransfer this worker-token.txt file to your Windows node at C:\k0s-token.txt.
2. Enable Windows Containers feature
Open PowerShell as Administrator on the Windows node and run:
Install-WindowsFeature -Name ContainersNote: A system reboot is required after enabling this feature.
3. Download and install k0s
After rebooting, open PowerShell as Administrator and run:
# Set variables
$K0S_VERSION = "v1.34.3+k0s.0"
$TOKEN_FILE = "C:\k0s-token.txt"
# Download k0s
$encodedVersion = $K0S_VERSION.Replace('+', '%2B')
$url = "https://github.com/k0sproject/k0s/releases/download/$encodedVersion/k0s-$K0S_VERSION-amd64.exe"
Invoke-WebRequest -Uri $url -OutFile "$env:LOCALAPPDATA\Microsoft\WindowsApps\k0s.exe"
# Install and start k0s worker
k0s install worker --token-file $TOKEN_FILE
k0s startNote: k0s supervises kubelet.exe and kube-proxy.exe.
Network connectivity in AWS
Disable the Change Source/Dest. Check option for the network interface attached to your EC2 instance. In AWS, the console option for the network interface is in the Actions menu.
Useful commands
Run pod with cmd.exe shell
kubectl run win --image=hello-world:nanoserver --command=true -i --attach=true -- cmd.exeManifest for pod with IIS web-server
apiVersion: v1
kind: Pod
metadata:
name: iis
spec:
containers:
- name: iis
image: mcr.microsoft.com/windows/servercore/iis
imagePullPolicy: IfNotPresentExtensions (`docs/extensions.md`)
Cluster extensions
k0s allows users to use extensions to extend cluster functionality.
At the moment the only supported type of extensions is helm based charts.
The default configuration has no extensions.
Helm based extensions
Configuration example
/* Detailed source-code truncated for AI context efficiency. */By using the configuration above, the cluster would:
- add stable and prometheus-community chart repositories
- install the
prometheus-community/prometheuschart of the specified version to thedefaultnamespace.
The chart installation is implemented by using the helm.k0sproject.io/Chart custom resource. k0s automatically converts charts defined in the k0s configuration into Chart resources. You can also create Chart resources directly for more flexible management.
The cluster has a controller which monitors Chart resources and supports the following operations:
- install
- upgrade
- delete
For security reasons, the cluster operates only on Chart resources in the kube-system namespace, however, the target namespace for releases can be any namespace.
Chart Resource Specification
apiVersion: helm.k0sproject.io/v1beta1
kind: Chart
metadata:
name: prometheus
namespace: kube-system
spec:
chartName: prometheus-community/prometheus
version: "14.6.1"
namespace: default
timeout: 20m
values: |
alertmanager:
persistentVolume:
enabled: false
repository:
url: https://prometheus-community.github.io/helm-charts
# Or use secret-based authentication:
# configFrom:
# secretRef:
# name: helm-creds
status:
releaseName: prometheus
version: "14.6.1"
appVersion: "2.31.1"
revision: 1
namespace: default
updated: "2024-01-15T10:30:45Z"The spec.repository field allows each Chart to include its own repository configuration, making Charts self-contained and portable. When using k0s configuration file, k0s automatically embeds the repository configuration into the generated Chart resources.
For more details on using Chart resources directly, see the Helm Charts documentation.
External Runtime Deps (`docs/external-runtime-deps.md`)
External runtime dependencies
k0s is packaged as a single binary, which includes all the needed components.
All the binaries are statically linked which means that in typical use cases
there's an absolute minimum of external runtime dependencies.
However, depending on the node role and cluster configuration, some of the
underlying components may have specific dependencies, like OS level tools,
packages and libraries. This page aims to provide a comprehensive overview.
The following command checks for known requirements on a host (currently only
available on Linux):
k0s sysinfoLinux specific
Linux kernel configuration
Needless to say, as k0s operates Kubernetes worker nodes, a certain number of
Linux kernel modules and configurations are required on the host. This
essentially stems from the requirement to run containers and set up networking
for them.
The needed kernel configuration items are listed below. All of them are
available in Kernel versions 4.3 and above. If running on older kernels, check
if the distro in use has backported some features; nevertheless, it might meet
the requirements. k0s will check the Linux kernel release as part of its
pre-flight checks and issue a warning if it's below 3.10.
The list covers ONLY the k0s/Kubernetes components' needs on worker nodes. Your
own workloads may require more.
CONFIG_CGROUPS:
Control Group supportCONFIG_CGROUP_SCHED:
Group CPU schedulerCONFIG_FAIR_GROUP_SCHED:
Group scheduling for SCHED_OTHER
kubernetes/kubeadm#2335 (comment)- (optional)
CONFIG_CFS_BANDWIDTH:
CPU bandwidth provisioning for FAIR_GROUP_SCHED
Required if CPU CFS quota enforcement is enabled for containers that
specify CPU limits (--cpu-cfs-quota).
- (optional)
- (optional)
CONFIG_BLK_CGROUP:
Block IO controller
kubernetes/kubernetes#92287 (comment)
CONFIG_NAMESPACES:
Namespaces supportCONFIG_UTS_NS:
UTS namespaceCONFIG_IPC_NS:
IPC namespaceCONFIG_PID_NS:
PID namespaceCONFIG_NET_NS:
Network namespace
CONFIG_NET:
Networking supportCONFIG_INET:
TCP/IP networkingCONFIG_NETFILTER:
Network packet filtering framework (Netfilter)- (optional)
CONFIG_NETFILTER_ADVANCED:
Advanced netfilter configuration CONFIG_NETFILTER_XTABLES:
Netfilter Xtables supportCONFIG_NETFILTER_XT_TARGET_REDIRECT:
REDIRECT target supportCONFIG_NETFILTER_XT_MATCH_COMMENT:
"comment" match support
- (optional)
CONFIG_EXT4_FS:
The Extended 4 (ext4) filesystemCONFIG_PROC_FS:
/proc file system support
Note: As part of its pre-flight checks, k0s will try to inspect and validate
the kernel configuration. In order for that to succeed, the configuration needs
to be accessible at runtime. There are some typical places that k0s will check.
A bullet-proof way to ensure the accessibility is to enableCONFIG_IKCONFIG_PROC,
and, if enabled as a module, to load the configs module: modprobe configs.
Control Groups (cgroups)
cgroup v2 is required by default, k0s will fail its pre-flight checks if it's
unavailable on the node.
As of Kubernetes 1.31, cgroup v1 is in maintenance mode (KEP-4569). As of
Kubernetes 1.35, the kubelet will fail to start by default if cgroup v2 is not
detected on the node. In order to run Kubernetes with cgroup v1, bypass the k0s
pre-flight checks via --ignore-pre-flight-checks and set failCgroupV1: false
in the kubelet configuration via the k0s worker profiles:
...
spec:
workerProfiles:
# By default, workers will pick up the default profile.
- name: default
values:
failCgroupV1: false!!! warning "Migrate to cgroup v2 now!"
The above escape hatch will not be available indefinitely, so plan
accordingly. KEP-5573 states:
> We will commit to removing cgroup v1 code but there is not yet a timeline for
> removal. The removal will be done no earlier than 1.38 to maintain the k8s
> deprecation policy.Required cgroup controllers:
cpucpuacctcpusetmemorydevicesfreezerpids
Optional cgroup controllers:
hugetlb(kubernetes/kubeadm#2335 (comment))blkio(kubernetes/kubernetes#92287 (comment))
containerd and cri-o will use blkio to track disk I/O and throttling in both
cgroup v1 and v2.
No integration with Name Service Switch (NSS) APIs
The k0s Linux binaries, including the ones distributed on the GitHub releases
pages, are by default statically linked and do not depend on the host's
C library. This ensures that k0s can run seamlessly across a wide range of
Linux environments. The k0s executable and most of the embedded binaries are
built without cgo and use the pure Go resolver, which only
consults the files and dns sources of /etc/nsswitch.conf. The Go
executables that do require cgo, such as containerd and runc, as well as all
non-Go executables, are statically linked against musl libc, which doesn't
implement NSS at all. In all cases, k0s cannot use glibc's NSS APIs, which
require dynamic linking.
This limitation is particularly relevant when a system uses NSS plugins, such as
nss-myhostname, for resolving network names like localhost. Systems lacking
a dedicated stub resolver capable of handling localhost DNS queries
specifically will encounter issues running k0s. To mitigate this, users are
advised to either activate a stub DNS resolver, such as systemd-resolved, or
to manually add localhost entries to the /etc/hosts file as shown below:
127.0.0.1 localhost
::1 localhostThe same limitation applies to systems that resolve all names via NSS, such as
through nss-resolve on hosts running systemd-resolved. Programs linked
against glibc can resolve names just fine on such systems, even if/etc/resolv.conf doesn't list any nameservers. In contrast, k0s and its
embedded components only ever consult /etc/resolv.conf. If that file has no
nameserver entries, they fall back to querying a nameserver on localhost, which
usually doesn't exist. This leaves k0s without working name resolution, although
the host itself appears fine. Make sure that /etc/resolv.conf lists reachable
nameservers. When using systemd-resolved, this typically means making/etc/resolv.conf a symbolic link to /run/systemd/resolve/stub-resolv.conf,
as described in the systemd-resolved documentation. Pods can't reach that stub
resolver on the host's loopback interface, which is why k0s configures the
kubelet to use systemd-resolved's uplink file /run/systemd/resolve/resolv.conf
for pods instead. The k0s pre-flight checks detect this situation by resolving a
name under the reserved invalid top-level domain, which any working resolver
answers with NXDOMAIN. If that lookup fails, k0s issues a warning. If/etc/resolv.conf doesn't list any nameservers on top of that, worker nodes
refuse to start. Controllers only warn in that case.
Kernel Parameters
Inotify Instance Limit
Inotify API allows
applications to monitor filesystem changes.
Workloads that run many pods or watch large numbers of files can exhaust the
node's default inotify limits, particularly fs.inotify.max_user_instances,
which limits how many inotify instances one user (UID) can have.
A value of at least 1024 is recommended; 8192 is commonly used in Kubernetes
environments:
fs.inotify.max_user_instances = 8192Check the current value:
bashcat /proc/sys/fs/inotify/max_user_instancesTo apply the new value persistently, create a
sysctlconfiguration file,
for example,/etc/sysctl.d/99-inotify.conf, and place the parameter
setting from above in it.Load the new kernel values:
bashsudo sysctl --system
External hard dependencies
There are very few external tools that are needed or used.
mount/umount
When setting up pods, kubelet will call mount binary on the host. Similarly when destroying pods it will call umount. mount and umount are only needed on worker nodes where kubelet runs.
External soft dependencies
There are a few external tools that may be needed or used under specific
circumstances:
containerd and AppArmor
In order to use containerd in conjunction with AppArmor, it must be enabled in
the kernel and the /sbin/apparmor_parser executable must be installed on the
host, otherwise containerd will disable AppArmor support.
iptables
iptables may be executed to detect if there are any existing iptables rules and
if those are in legacy or nft mode. If iptables is not found, k0s will assume
that there are no pre-existing iptables rules.
useradd / adduser
During k0s install the external tool useradd will be used on the controllers
to create system user accounts for k0s. If this doesn't exist it will fall-back to
busybox's adduser.
userdel / deluser
k0s reset will execute either userdel or deluser to clean up system user
accounts.
modprobe
On k0s worker modprobe will be executed to load missing kernel modules if they
are not detected.
id
External id will be executed as a fallback if local user lookup fails, in case
NSS is used.
Windows specific
TBD.
Feature Gates (`docs/feature-gates.md`)
Feature Gates
Feature gates are a mechanism to enable or disable experimental features in k0s.
They allow you to control which features are active in your cluster, providing
a way to safely test new functionality before it becomes generally available.
Overview
Feature gates in k0s follow a lifecycle model similar to Kubernetes:
- Alpha: Experimental features that are disabled by default and may be unstable or removed
- Beta: Features that are more stable but still being tested, enabled by default
- GA (Generally Available): Stable features that cannot be disabled. If explicitly disabled
k0s will fail to start
Once a feature reaches GA status, the feature gate cannot be disabled anymore and it will be
eventually removed.
Using Feature Gates
Feature gates can be configured using the --feature-gates flag when starting the k0s controllers:
k0s controller --feature-gates="ExampleFeature=true,AnotherFeature=false"The feature gates configuration must be consistent across all controllers in the cluster.
Important notes:
- Currently feature gates apply only to the controllers. They may be added to workers in future releases
- Setting a GA feature gate to false will cause k0s startup to fail
- Unknown feature gates will cause k0s start up to fail
- Feature gates use case-sensitive CamelCase names
Available Features
The following table is a summary of the feature gates that you can set. Each feature is designed for a specific stage of stability:
| Feature | Default | Stage | Since | Until |
|---|---|---|---|---|
IPv6SingleStack |
false |
Alpha | 1.34 |
Feature Details
IPv6SingleStack: Enables single-stack IPv6 support in k0s clusters.
Helm Charts (`docs/helm-charts.md`)
Helm Charts
k0s provides built-in support for deploying applications using Helm charts. You can deploy charts using:
- Chart custom resources (recommended) - Self-contained Kubernetes resources that include repository configuration
- k0s configuration file - Centralized configuration for bootstrap and cluster-level management
- Helm CLI - Standard Helm commands work with k0s clusters
Using Chart Custom Resources (Recommended)
The recommended way to deploy Helm charts in k0s is by creating Chart custom resources directly. This approach provides:
- Self-contained configuration - Each Chart includes its repository configuration
- GitOps-friendly - Manage charts as standard Kubernetes resources
- Flexible credentials - Different charts can use different repository credentials
- Portable - Charts can be moved between clusters easily
- No k0s restart needed - Apply changes instantly with
kubectl
Quick Start
Create a Chart resource to deploy Prometheus:
apiVersion: helm.k0sproject.io/v1beta1
kind: Chart
metadata:
name: prometheus
namespace: kube-system
spec:
chartName: prometheus-community/prometheus
version: "14.6.1"
namespace: default
values: |
alertmanager:
persistentVolume:
enabled: false
repository:
url: https://prometheus-community.github.io/helm-chartsApply it: kubectl apply -f prometheus-chart.yaml
Chart Resource Configuration
Spec Fields
| Field | Default value | Description |
|---|---|---|
chartName |
(required) | Chart reference: repo/chart, oci://registry/chart, or /path/to/chart.tgz |
version |
(required) | Chart version to install |
namespace |
(required) | Target namespace for the release |
releaseName |
- | Helm release name (defaults to Chart resource name) |
values |
- | Custom chart values as YAML formatted string |
timeout |
10m |
Timeout to wait for release install |
forceUpgrade |
true |
When set to false, disables the use of the --force flag when upgrading |
repository |
- | Repository configuration (see below) |
Repository Configuration
| Field | Description |
|---|---|
url |
Repository URL (required for traditional repos, omit for OCI) |
username |
Username for Basic HTTP authentication |
password |
Password for Basic HTTP authentication |
caFile |
CA bundle file path for HTTPS verification |
certFile |
TLS certificate file path for mTLS |
keyFile |
TLS key file path for mTLS |
insecure |
Skip TLS certificate checks (default: false) |
Note: For traditional Helm repositories, only repositories providing a valid index.yaml are supported. See the Helm repository documentation for details.
Examples
Traditional Helm Repository
apiVersion: helm.k0sproject.io/v1beta1
kind: Chart
metadata:
name: nginx-ingress
namespace: kube-system
spec:
chartName: ingress-nginx/ingress-nginx
version: "4.0.0"
namespace: ingress-nginx
repository:
url: https://kubernetes.github.io/ingress-nginxWith Authentication
apiVersion: helm.k0sproject.io/v1beta1
kind: Chart
metadata:
name: private-chart
namespace: kube-system
spec:
chartName: myrepo/mychart
version: "1.0.0"
namespace: default
repository:
url: https://charts.example.com
username: myuser
password: mytokenOCI Registry (Public)
apiVersion: helm.k0sproject.io/v1beta1
kind: Chart
metadata:
name: oci-chart
namespace: kube-system
spec:
chartName: oci://ghcr.io/org/chart
version: "0.5.0"
namespace: default
# No repository field needed for public OCI registriesOCI Registry with Authentication
apiVersion: helm.k0sproject.io/v1beta1
kind: Chart
metadata:
name: private-oci-chart
namespace: kube-system
spec:
chartName: oci://registry.example.com:5000/charts/app
version: "1.0.0"
namespace: default
repository:
# URL not needed for OCI - it's in chartName
username: robot-account
password: secret-tokenOCI Registry with Custom CA and mTLS
apiVersion: helm.k0sproject.io/v1beta1
kind: Chart
metadata:
name: registry-with-tls
namespace: kube-system
spec:
chartName: oci://registry.internal.com:8080/charts/app
version: "1.0.0"
namespace: default
repository:
caFile: /etc/k0s/pki/registry-ca.crt
certFile: /etc/k0s/pki/client.crt
keyFile: /etc/k0s/pki/client.keyLocal Chart File
apiVersion: helm.k0sproject.io/v1beta1
kind: Chart
metadata:
name: local-chart
namespace: kube-system
spec:
chartName: /var/lib/k0s/charts/mychart-1.0.0.tgz
version: "1.0.0"
namespace: default
# No repository field needed for local filesSecret-Based Authentication
For sensitive credentials, use Kubernetes Secrets instead of embedding them in Chart resources:
# Create a secret with repository credentials
kubectl create secret generic helm-registry-creds \
--from-literal=username=myuser \
--from-literal=password=mypassword \
-n kube-systemapiVersion: helm.k0sproject.io/v1beta1
kind: Chart
metadata:
name: app-from-private-registry
namespace: kube-system
spec:
chartName: oci://registry.example.com/charts/app
version: "1.0.0"
namespace: default
repository:
configFrom:
secretRef:
name: helm-registry-creds
# namespace defaults to Chart's namespace if omittedSecret Keys:
url- Repository URLusername- HTTP Basic auth usernamepassword- HTTP Basic auth passwordca.crt- CA certificate bundle (PEM format)tls.crt- Client TLS certificate (PEM format)tls.key- Client TLS private key (PEM format)insecure- Set to "true" to skip TLS verification
Precedence: Secret values override inline repository fields when present. Inline fields provide defaults when corresponding secret keys are missing or empty.
Example with defaults:
apiVersion: helm.k0sproject.io/v1beta1
kind: Chart
metadata:
name: app-with-defaults
namespace: kube-system
spec:
chartName: oci://registry.example.com/charts/app
version: "1.0.0"
namespace: default
repository:
url: oci://registry-fallback.local/charts/app # Used only if secret doesn't provide 'url'
username: default-user # Used only if secret doesn't provide 'username'
configFrom:
secretRef:
name: helm-registry-creds # Secret values always override inline fieldsChart Lifecycle
Charts are automatically managed by k0s:
- Install: When a Chart resource is created, k0s installs the Helm release
- Upgrade: When Chart spec changes (version, values), k0s upgrades the release
- Uninstall: When Chart resource is deleted, k0s uninstalls the release
Monitor chart status:
kubectl get charts -n kube-system
kubectl describe chart prometheus -n kube-systemNote: Chart resources must be created in the kube-system namespace for security reasons, but can deploy releases to any target namespace.
Using k0s Configuration (Alternative Method)
For cluster-level Helm chart management during bootstrap or centralized configuration, you can define charts in the k0s configuration file. k0s automatically converts these into Chart custom resources.
This approach:
- Requires modifying
k0s.yamland restarting k0s controllers - Useful for bootstrap and initial cluster setup
- Centralized configuration for all charts
Configuration Structure
Chart install and upgrade options
Charts are processed with the following Helm options by default:
--create-namespace--atomic--force(only for theupgradecommand)--wait--wait-for-jobs
See the configuration tables below for how to customize these options.
Repository configuration
| Field | Default value | Description |
|---|---|---|
name |
(required) | The repository name |
url |
(required) | The repository URL |
insecure |
true |
Whether to skip TLS certificate checks when connecting to the repository |
caFile |
- | CA bundle file to use when verifying HTTPS-enabled servers |
certFile |
- | The TLS certificate file to use for HTTPS client authentication (mTLS) |
keyFile |
- | The TLS key file to use for HTTPS client authentication (mTLS) |
username |
- | Username for Basic HTTP authentication |
password |
- | Password for Basic HTTP authentication |
Note: k0s supports only classic Helm chart repositories that provide a valid index.yaml. Direct links to chart folders or files (for example raw GitHub URLs) are not recognized as Helm repositories and will not work unless they follow the full Helm repository structure. For details on how a valid Helm chart repository must be structured, see: https://helm.sh/docs/topics/chart_repository/#create-a-chart-repository
Chart configuration
| Field | Default value | Description |
|---|---|---|
name |
- | Release name |
chartname |
- | Chart name in form repository/chartname or path to tgz file |
version |
- | Chart version to install |
timeout |
- | Timeout to wait for release install |
values |
- | Custom chart values as YAML formatted string |
namespace |
- | Namespace to install the chart into |
forceUpgrade |
true |
When set to false, disables the use of the --force flag when upgrading the chart |
order |
0 |
Order in which to apply the manifest. For equal values, alphanumeric ordering is used. |
Example k0s.yaml Configuration
In this example, Prometheus is configured from the prometheus-community Helm chart repository:
spec:
extensions:
helm:
concurrencyLevel: 5
repositories:
- name: stable
url: https://charts.helm.sh/stable
- name: prometheus-community
url: https://prometheus-community.github.io/helm-charts
- name: helm-repo-with-auth
url: https://charts.example.com
username: myuser
password: mytoken
charts:
- name: prometheus-stack
chartname: prometheus-community/prometheus
version: "14.6.1"
timeout: 20m
order: 1
values: |
alertmanager:
persistentVolume:
enabled: false
server:
persistentVolume:
enabled: false
namespace: defaultAdd this to your k0s.yaml and restart k0s controllers. The charts will deploy automatically.
Migrating from k0s Config to Chart Resources
To migrate from k0s.yaml configuration to Chart custom resources:
For each chart in your
spec.extensions.helm.chartssection:- Find the matching repository from
spec.extensions.helm.repositories - Create a Chart resource with the repository configuration embedded
- Apply the Chart resource:
kubectl apply -f chart.yaml
- Find the matching repository from
Optionally remove charts from k0s.yaml (both methods can coexist)
Example migration:
Before (k0s.yaml):
spec:
extensions:
helm:
repositories:
- name: prometheus-community
url: https://prometheus-community.github.io/helm-charts
charts:
- name: prometheus
chartname: prometheus-community/prometheus
version: "14.6.1"
namespace: defaultAfter (Chart resource):
apiVersion: helm.k0sproject.io/v1beta1
kind: Chart
metadata:
name: prometheus
namespace: kube-system
spec:
chartName: prometheus-community/prometheus
version: "14.6.1"
namespace: default
repository:
url: https://prometheus-community.github.io/helm-chartsPopular Chart Examples
Example extensions that work well with k0s:
- Ingress controllers: nginx ingress, Traefik ingress (refer to the k0s documentation for Installing the Traefik Ingress Controller)
- Volume storage providers: OpenEBS, Rook, Longhorn
- Monitoring: Prometheus, Grafana
Helm debug logging
Running k0s controller with --debug=true enables helm debug logging.
High Availability (`docs/high-availability.md`)
Control Plane High Availability
You can create high availability for the control plane by distributing the control plane across multiple nodes and installing a load balancer on top. Etcd can be co-located with the controller nodes (default in k0s) to achieve highly available data store at the same time.
Note: In this context, a two-node control plane is considered highly
available, even though it is not HA from etcd's perspective. The same
requirement for the load balancer still applies.
Network considerations
You should plan to allocate the control plane nodes into different zones. This will avoid failures in case one zone fails.
For etcd high availability it's recommended to configure 3 or 5 controller nodes. For more information, refer to the etcd documentation.
Load Balancer
Control plane high availability requires a tcp load balancer, which acts as a single point of contact to access the controllers. The load balancer needs to allow and route traffic to each controller through the following ports:
- 6443 (for Kubernetes API)
- 8132 (for Konnectivity)
- 9443 (for controller join API)
The load balancer can be implemented in many different ways and k0s doesn't have any additional requirements. You can use for example HAProxy, nginx or your cloud provider's load balancer.
Example configuration: HAProxy
Add the following lines at the end of haproxy.cfg:
frontend kubeAPI
bind :6443
mode tcp
default_backend kubeAPI_backend
frontend konnectivity
bind :8132
mode tcp
default_backend konnectivity_backend
frontend controllerJoinAPI
bind :9443
mode tcp
default_backend controllerJoinAPI_backend
backend kubeAPI_backend
mode tcp
server k0s-controller1 <ip-address1>:6443 check check-ssl verify none
server k0s-controller2 <ip-address2>:6443 check check-ssl verify none
server k0s-controller3 <ip-address3>:6443 check check-ssl verify none
backend konnectivity_backend
mode tcp
server k0s-controller1 <ip-address1>:8132 check check-ssl verify none
server k0s-controller2 <ip-address2>:8132 check check-ssl verify none
server k0s-controller3 <ip-address3>:8132 check check-ssl verify none
backend controllerJoinAPI_backend
mode tcp
server k0s-controller1 <ip-address1>:9443 check check-ssl verify none
server k0s-controller2 <ip-address2>:9443 check check-ssl verify none
server k0s-controller3 <ip-address3>:9443 check check-ssl verify none
listen stats
bind *:9000
mode http
stats enable
stats uri /The last block "listen stats" is optional, but can be helpful. It enables HAProxy statistics with a separate dashboard to monitor for example the health of each backend server. You can access it using a web browser:
http://<ip-addr>:9000Restart HAProxy to apply the configuration changes.
k0s configuration
First and foremost, all controllers should utilize the same CA certificates and SA key pair:
/var/lib/k0s/pki/ca.key
/var/lib/k0s/pki/ca.crt
/var/lib/k0s/pki/sa.key
/var/lib/k0s/pki/sa.pub
/var/lib/k0s/pki/etcd/ca.key
/var/lib/k0s/pki/etcd/ca.crtTo generate these certificates, you have two options: either generate them
manually using the instructions for installing custom CA certificates, and
then share them between controller nodes, or use k0sctl to generate and share
them automatically.
The second important aspect is: the load balancer address must be configured to k0s either by using k0s.yaml or by using k0sctl to automatically deploy all controllers with the same configuration:
Configuration using k0s.yaml (for each controller)
Note to update your load balancer's public IP address into two places.
spec:
api:
externalAddress: <load balancer public ip address>Configuration using k0sctl.yaml (for k0sctl)
Add the following lines at the end of k0sctl.yaml. Note to update your load
balancer's public IP address into two places.
k0s:
config:
spec:
api:
externalAddress: <load balancer public ip address>For greater detail about k0s configuration, refer to the Full configuration file reference.
Install (`docs/install.md`)
Quick Start Guide
Completing this Quick Start results in a single-node Kubernetes cluster that
incorporates both the controller and worker roles. This setup is ideal for
environments that don't require high availability or multiple nodes.
Prerequisites
Note: Before proceeding, make sure to review the System Requirements.
The following steps work on every typical Linux distribution that uses either
systemd or OpenRC as its init system.
Install k0s
Download k0s
Run the k0s download script to download the latest stable version of k0s and make it executable from /usr/local/bin/k0s.
shellcurl --proto '=https' --tlsv1.2 -sSf https://get.k0s.sh | sudo shAlternatively, download it from the k0s releases page. This approach is
required in air gapped environments.Install k0s as a service
The
k0s installsub-command installs k0s as a system service on a host that is running one of the supported init systems: systemd or OpenRC. You can execute the install for workers, controllers or single node (controller+worker) instances.Run the following command to install a single node k0s that includes the controller and worker functions with the default configuration:
shellsudo k0s install controller --singleNote: The
--singleoption disables features needed for multi-node
clusters, so the cluster cannot be extended. To retain the ability to expand
the cluster in the future, use:shellsudo k0s install controller --enable-worker --no-taintsThe
k0s install controllersub-command accepts the same flags and parameters as thek0s controller. Refer to manual install for a custom config file example.It is possible to set environment variables with the install command:
shellsudo k0s install controller -e ETCD_UNSUPPORTED_ARCH=armThe system service can be reinstalled with the
--forceflag:shellsudo k0s install controller --single --force sudo systemctl daemon-reloadIf you need to reinstall and immediately start the service, combine
--forcewith--start:shellsudo k0s install controller --single --force --startWhen used with
--start, the--forceflag will also restart an already running service.Start k0s as a service
To start the k0s service, run:
shellsudo k0s startAlternatively, both steps can be combined by using the
--startflag with the install command:shellsudo k0s install controller --single --startThe k0s service will start automatically after the node restart.
A minute or two typically passes before the node is ready to deploy applications.
Check service, logs and k0s status
Run the following command to check the k0s instance status:
shell$ sudo k0s status Version: {{{ k0s_version }}} Process ID: 436 Role: controller Workloads: true Init System: linux-systemdAccess the cluster using kubectl
Note: k0s embeds the Kubernetes command-line tool kubectl.
Use kubectl to deploy applications or check node status:
shell$ sudo k0s kubectl get nodes NAME STATUS ROLES AGE VERSION k0s Ready <none> 4m6s {{{ k8s_version }}}+k0s
Uninstall k0s
The removal of k0s is a two-step process.
Stop the service.
shellsudo k0s stopExecute the
k0s resetcommand.The
k0s resetcommand cleans up the installed system service, data directories, containers, mounts and network namespaces.shellsudo k0s resetReboot the system.
A few small k0s fragments persist even after the reset, such as iptables
rules. Reboot the machine after runningk0s reset.
Next Steps
- Install using k0sctl: Deploy multi-node clusters using just one command
- Manual Install: (Advanced) Manually deploy multi-node clusters
- Control plane configuration options: Networking and data store configuration
- Worker node configuration options: Node labels and kubelet arguments
- Support for cloud providers: Load balancer or storage configuration
- Installing the Traefik Ingress Controller:
Ingress deployment information - Air gapped/offline installation: Air gaped deployment
K0s In Docker (`docs/k0s-in-docker.md`)
Run k0s in Docker
You can create a k0s cluster on top of Docker.
Prerequisites
You will require a Docker environment running on a Mac, Windows, or Linux system.
Container images
The k0s OCI images are published to both Docker Hub and GitHub Container
registry. For simplicity, the examples given here use Docker Hub (GitHub
requires separate authentication, which is not covered here). The image names
are as follows:
docker.io/k0sproject/k0s:{{{ k0s_docker_version }}}ghcr.io/k0sproject/k0s:{{{ k0s_docker_version }}}
Note: Due to Docker's tag validation scheme, - is used as the k0s version
separator instead of the usual +. For example, k0s version {{{ k0s_version }}} is tagged as docker.io/k0sproject/k0s:{{{ k0s_docker_version }}}.
Start k0s
1. Run a controller
By default, running the k0s OCI image will launch a controller with workloads
enabled (i.e. a controller with the --enable-worker flag) to provide an easy
local testing "cluster":
docker run -d --name k0s-controller --hostname k0s-controller \
-v /var/lib/k0s -v /var/log/pods `# this is where k0s stores its data` \
--tmpfs /run `# this is where k0s stores runtime data` \
--privileged `# this is the easiest way to enable container-in-container workloads` \
-p 6443:6443 `# publish the Kubernetes API server port` \
docker.io/k0sproject/k0s:{{{ k0s_docker_version }}}Explanation of command line arguments:
-druns the container in detached mode, i.e. in the background.--name k0s-controllernames the container "k0s-controller".--hostname k0s-controllersets the host name of the container to
"k0s-controller".-v /var/lib/k0s -v /var/log/podscreates two Docker volumes and mounts them
to/var/lib/k0sand/var/log/podsrespectively inside the container,
ensuring that cluster data persists across container restarts.--tmpfs /runTODO--privilegedgives the container the elevated privileges that k0s needs to
function properly within Docker. See the section on adding additional
workers for a more detailed discussion of
privileges.-p 6443:6443exposes the container's Kubernetes API server port 6443 to the
host, allowing you to interact with the cluster externally.docker.io/k0sproject/k0s:{{{ k0s_docker_version }}}is the name of the
k0s image to run.
By default, the k0s image starts a k0s controller with worker components enabled
within the same container, creating a cluster with a single
controller-and-worker node using the following command:
{% include "../Dockerfile" start="# Start CMD" end="# End CMD" %}Alternatively, a controller-only node can be run like this:
docker run -d --name k0s-controller --hostname k0s-controller \
--read-only `# k0s won't write any data outside the below paths` \
-v /var/lib/k0s `# this is where k0s stores its data` \
--tmpfs /run `# this is where k0s stores runtime data` \
--tmpfs /tmp `# allow writing temporary files` \
-p 6443:6443 `# publish the Kubernetes API server port` \
docker.io/k0sproject/k0s:{{{ k0s_docker_version }}} \
k0s controllerNote the addition of k0s controller to override the image's default command.
Also note that a controller-only node requires fewer privileges.
2. (Optional) Add additional workers
You can add multiple worker nodes to the cluster and then distribute your
application containers to separate workers.
Acquire a join token for the
worker:shtoken=$(docker exec k0s-controller k0s token create --role=worker)Run the container to create and join the new worker:
shdocker run -d --name k0s-worker1 --hostname k0s-worker1 \ -v /var/lib/k0s -v /var/log/pods `# this is where k0s stores its data` \ --tmpfs /run `# this is where k0s stores runtime data` \ --privileged `# this is the easiest way to enable container-in-container workloads` \ docker.io/k0sproject/k0s:{{{ k0s_docker_version }}} \ k0s worker $tokenAlternatively, with fine-grained privileges:
shdocker run -d --name k0s-worker1 --hostname k0s-worker1 \ -v /var/lib/k0s -v /var/log/pods `# this is where k0s stores its data` \ --tmpfs /run `# this is where k0s stores runtime data` \ --security-opt seccomp=unconfined \ --device /dev/kmsg \ --cap-add sys_admin \ --cap-add net_admin \ --cap-add sys_ptrace \ --cap-add sys_resource \ --cap-add syslog \ docker.io/k0sproject/k0s:{{{ k0s_docker_version }}} \ k0s worker "$token"Notes on the security-related flags:
--security-opt seccomp=unconfinedis required forruncto access the
session keyring.--device /dev/kmsgmakes/dev/kmsgvisible from inside the container.
The kubelet's OOM watcher uses this.
Notes on Linux capabilities:
CAP_SYS_ADMINallows for a variety of administrative tasks, including
mounting file systems and managing namespaces, which are necessary for
creating and configuring nested containers.CAP_NET_ADMINallows manipulation of network settings such as interfaces
and routes, allowing containers to create isolated or bridged networks, and
so on.CAP_SYS_PTRACEallows to inspect and modify processes, used to monitor
other containers in a nested environment.CAP_SYS_RESOURCEallows containers to override resource limits for things
like memory or file descriptors, used to manage and adjust resource
allocation in nested container environments.CAP_SYSLOGallows containers to perform privileged syslog operations.
This is required in order to read/dev/kmsg.
Note that more privileges may be required depending on your cluster
configuration and workloads.Repeat this step for each additional worker node and adjust the container and
host names accordingly. Make sure that the workers can reach the controller
on the required ports. If you are using Docker's default bridged network,
this should be the case.
3. Access your cluster
a) Using kubectl within the container
To check cluster status and list nodes, use:
docker exec k0s-controller k0s kubectl get nodesb) Using kubectl locally
To configure local access to your k0s cluster, follow these steps:
Generate the kubeconfig:
shdocker exec k0s-controller k0s kubeconfig admin > ~/.kube/k0s.configUpdate kubeconfig with Localhost Access:
To automatically replace the server IP with localhost dynamically in
~/.kube/k0s.config, use the following command:shsed -i '' -e "$(awk '/server:/ {print NR; exit}' ~/.kube/k0s.config)s|https://.*:6443|https://localhost:6443|" ~/.kube/k0s.configThis command updates the kubeconfig to point to localhost, allowing access to the API server from your host machine
Set the KUBECONFIG Environment Variable:
shexport KUBECONFIG=~/.kube/k0s.configVerify Cluster Access:
shkubectl get nodes
c) Use [Lens]
Access the k0s cluster using Lens by following the instructions on how to add a
cluster.
Use Docker Compose (alternative)
As an alternative you can run k0s using Docker Compose:
{% include "compose.yaml" %}Below is a more complex example, using Traefik as a load balancer, along with
three controller and three worker nodes:
{% include "compose-cluster.yaml" %}Running the above:
❯ docker compose up -d
[+] Running 11/11
✔ Network compose-cluster_k0s-net Created 0.1s
✔ Volume "compose-cluster_k0s-token-secrets" Created 0.0s
✔ Volume "compose-cluster_k0s-controller-token" Created 0.0s
✔ Volume "compose-cluster_k0s-worker-token" Created 0.0s
✔ Container k0s-lb Started 0.5s
✔ Container k0s-controller-1 Started 11.8s
✔ Container k0s-controller-2 Started 12.2s
✔ Container k0s-worker-1 Started 12.4s
✔ Container k0s-worker-2 Started 12.3s
✔ Container k0s-worker-3 Started 12.1s
✔ Container k0s-controller-3 Started 12.5sAfter a short while:
$ docker exec k0s-controller-1 k0s kc get node,po -A
NAME STATUS ROLES AGE VERSION
node/k0s-worker-1 Ready <none> 1m36s {{{ k8s_version }}}+k0s
node/k0s-worker-2 Ready <none> 1m36s {{{ k8s_version }}}+k0s
node/k0s-worker-3 Ready <none> 1m36s {{{ k8s_version }}}+k0s
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system pod/coredns-7d4f7fbd5c-54lxp 1/1 Running 0 1m27s
kube-system pod/coredns-7d4f7fbd5c-pwbck 1/1 Running 0 1m27s
kube-system pod/konnectivity-agent-5g8pn 1/1 Running 0 1m22s
kube-system pod/konnectivity-agent-6rp7r 1/1 Running 0 1m22s
kube-system pod/konnectivity-agent-zx9fn 1/1 Running 0 1m22s
kube-system pod/kube-proxy-9m77t 1/1 Running 0 1m36s
kube-system pod/kube-proxy-v5vs6 1/1 Running 0 1m36s
kube-system pod/kube-proxy-xfw2h 1/1 Running 0 1m36s
kube-system pod/kube-router-6c62v 1/1 Running 0 1m36s
kube-system pod/kube-router-98ss8 1/1 Running 0 1m36s
kube-system pod/kube-router-lr46f 1/1 Running 0 1m36s
kube-system pod/metrics-server-7778865875-fzhx6 1/1 Running 0 1m37sKnown limitations
No custom Docker networks
Currently, k0s nodes cannot be run if the containers are configured to use custom networks (for example, with --net my-net). This is because Docker sets up a custom DNS service within the network which creates issues with CoreDNS. No completely reliable workarounds are available, however no issues should arise from running k0s cluster(s) on a bridge network.
Next Steps
- Install using k0sctl: Deploy multi-node clusters using just one command
- Control plane configuration options: Networking and data store configuration
- Worker node configuration options: Node labels and kubelet arguments
- Support for cloud providers: Load balancer or storage configuration
- Installing the Traefik Ingress Controller: Ingress deployment information
K0s Multi Node (`docs/k0s-multi-node.md`)
Manual Install (Advanced)
You can manually set up k0s nodes by creating a multi-node cluster that is locally managed on each node. This involves several steps, to first install each node separately, and to then connect the node together using access tokens.
Prerequisites
Note: Before proceeding, make sure to review the System Requirements.
The following steps work on every typical Linux distribution that uses either
systemd or OpenRC as its init system.
You can speed up the use of the k0s command by enabling shell completion.
Install k0s
1. Download k0s
Run the k0s download script to download the latest stable version of k0s and make it executable from /usr/bin/k0s.
curl --proto '=https' --tlsv1.2 -sSf https://get.k0s.sh | sudo shThe download script accepts the following environment variables:
| Variable | Purpose |
|---|---|
K0S_VERSION={{{ k0s_version }}} |
Select the version of k0s to be installed |
DEBUG=true |
Output commands and their arguments at execution. |
Note: If you require environment variables and use sudo, you can do:
curl --proto '=https' --tlsv1.2 -sSf https://get.k0s.sh | sudo K0S_VERSION={{{ k0s_version }}} sh2. Bootstrap a controller node
Create a configuration file:
mkdir -p /etc/k0s
k0s config create > /etc/k0s/k0s.yamlNote: For information on settings modification, refer to the configuration documentation.
sudo k0s install controller -c /etc/k0s/k0s.yamlsudo k0s startAlternatively, both steps can be combined with the --start flag:
sudo k0s install controller -c /etc/k0s/k0s.yaml --startThe system service can be reinstalled with the --force flag. When combined with --start, it will also restart an already running service:
sudo k0s install controller -c /etc/k0s/k0s.yaml --force --startk0s process acts as a "supervisor" for the control plane components. In moments the control plane will be up and running.
3. Create a join token
You need a token to join workers to the cluster. The token embeds information that enables mutual trust between the worker and controller(s) and which allows the node to join the cluster as worker.
To get a token, run the following command on one of the existing controller nodes:
sudo k0s token create --role=workerThe resulting output is a long token string, which you can use to add a worker to the cluster.
For enhanced security, run the following command to set an expiration time for the token:
sudo k0s token create --role=worker --expiry=100h > token-file4. Add workers to the cluster
To join the worker, run k0s in the worker mode with the join token you created:
sudo k0s install worker --token-file /path/to/token/filesudo k0s startAlternatively, both steps can be combined with the --start flag:
sudo k0s install worker --token-file /path/to/token/file --startThe system service can be reinstalled with the --force flag. When combined with --start, it will also restart an already running service:
sudo k0s install worker --token-file /path/to/token/file --force --startAbout join tokens
The join tokens are base64-encoded kubeconfigs for several reasons:
- Well-defined structure
- Capable of direct use as bootstrap auth configs for kubelet
- Embedding of CA info for mutual trust
The bearer token embedded in the kubeconfig is a bootstrap token. For controller join tokens and worker join tokens k0s uses different usage attributes to ensure that k0s can validate the token role on the controller side.
5. Add controllers to the cluster
Note: Either etcd or an external data store (MySQL or PostgreSQL) via kine must be in use to add new controller nodes to the cluster. Pay strict attention to the high availability configuration and make sure the configuration is identical for all controller nodes.
To create a join token for the new controller, run the following command on an existing controller:
sudo k0s token create --role=controller --expiry=1h > token-fileOn the new controller, run:
sudo k0s install controller --token-file /path/to/token/file -c /etc/k0s/k0s.yamlImportant notice here is that each controller in the cluster must have k0s.yaml otherwise some cluster nodes will use default config values which will lead to inconsistency behavior.
If your configuration file includes IP addresses (node address, SANs, etcd peer address), remember to update them accordingly for this specific controller node.
sudo k0s startAlternatively, both steps can be combined with the --start flag:
sudo k0s install controller --token-file /path/to/token/file -c /etc/k0s/k0s.yaml --startThe system service can be reinstalled with the --force flag. When combined with --start, it will also restart an already running service:
sudo k0s install controller --token-file /path/to/token/file -c /etc/k0s/k0s.yaml --force --start6. Check k0s status
Run the following command to check the k0s instance status:
sudo k0s statusVersion: {{{ k0s_version }}}
Process ID: 2769
Parent Process ID: 1
Role: controller
Init System: linux-systemd
Service file: /etc/systemd/system/k0scontroller.service7. Access your cluster
Use the Kubernetes 'kubectl' command-line tool that comes with k0s binary to deploy your application or check your node status:
sudo k0s kubectl get nodesNAME STATUS ROLES AGE VERSION
k0s Ready <none> 4m6s {{{ k8s_version }}}+k0sYou can also access your cluster easily with Lens, simply by copying the kubeconfig and pasting it to Lens:
sudo cat /var/lib/k0s/pki/admin.confNote: To access the cluster from an external network you must replace localhost in the kubeconfig with the host ip address for your controller.
Next Steps
- Install using k0sctl: Deploy multi-node clusters using just one command
- Control plane configuration options: Networking and data store configuration
- Worker node configuration options: Node labels and kubelet arguments
- Support for cloud providers: Load balancer or storage configuration
- Installing the Traefik Ingress Controller: Ingress deployment information
K0sctl Install (`docs/k0sctl-install.md`)
Install using k0sctl
k0sctl is a command-line tool for bootstrapping and managing k0s clusters. k0sctl connects to the provided hosts using SSH and gathers information on the hosts, with which it forms a cluster by configuring the hosts, deploying k0s, and then connecting the k0s nodes together.
With k0sctl, you can create multi-node clusters in a manner that is automatic and easily repeatable. This method is recommended for production cluster installation.
Note: The k0sctl install method is necessary for automatic upgrade.
Prerequisites
You can execute k0sctl on any system that supports the Go language. Pre-compiled k0sctl binaries are available on the k0sctl releases page).
Note: For target host prerequisites information, refer to the k0s System Requirements.
Install k0s
1. Install k0sctl tool
k0sctl is a single binary, the instructions for downloading and installing of which are available in the k0sctl github repository.
2. Configure the cluster
Run the following command to create a k0sctl configuration file:
shellk0sctl init > k0sctl.yamlThis action creates a
k0sctl.yamlfile in the current directory:yamlapiVersion: k0sctl.k0sproject.io/v1beta1 kind: Cluster metadata: name: k0s-cluster spec: hosts: - role: controller ssh: address: 10.0.0.1 # replace with the controller's IP address user: root keyPath: ~/.ssh/id_rsa - role: worker ssh: address: 10.0.0.2 # replace with the worker's IP address user: root keyPath: ~/.ssh/id_rsaProvide each host with a valid IP address that is reachable by k0sctl, and the connection details for an SSH connection.
Note: Refer to the k0sctl documentation for k0sctl configuration specifications.
3. Deploy the cluster
Run k0sctl apply to perform the cluster deployment:
/* Detailed source-code truncated for AI context efficiency. */4. Access the cluster
To access your k0s cluster, use k0sctl to generate a kubeconfig for the purpose.
k0sctl kubeconfig > kubeconfigWith the kubeconfig, you can access your cluster using either kubectl or Lens.
kubectl get pods --kubeconfig kubeconfig -ANAMESPACE NAME READY STATUS RESTARTS AGE
kube-system calico-kube-controllers-5f6546844f-w8x27 1/1 Running 0 3m50s
kube-system calico-node-vd7lx 1/1 Running 0 3m44s
kube-system coredns-5c98d7d4d8-tmrwv 1/1 Running 0 4m10s
kube-system konnectivity-agent-d9xv2 1/1 Running 0 3m31s
kube-system kube-proxy-xp9r9 1/1 Running 0 4m4s
kube-system metrics-server-6fbcd86f7b-5frtn 1/1 Running 0 3m51sKnown limitations
- k0sctl does not perform any discovery of hosts, and thus it only operates on the hosts listed in the provided configuration.
- k0sctl can only add more nodes to the cluster. It cannot remove existing nodes.
Next Steps
- Control plane configuration options: Networking and datastore configuration
- Worker node configuration options: Node labels and kubelet arguments
- Support for cloud providers: Load balancer or storage configuration
- Installing the Traefik Ingress Controller: Ingress deployment information
Manifests (`docs/manifests.md`)
Manifest Deployer
Included with k0s, Manifest Deployer is one of two methods you can use to run k0s with your preferred extensions (the other being by defining your extensions as Helm charts).
Overview
Manifest Deployer runs on the controller nodes and provides an easy way to automatically deploy manifests at runtime.
By default, k0s reads all manifests under /var/lib/k0s/manifests that end with .yaml and ensures that their state matches the cluster state. It does not read files with other extensions, such as .yml. Moreover, on removal of a manifest file, k0s will automatically prune all of its associated resources.
The use of Manifest Deployer is quite similar to the use the kubectl apply command. The main difference between the two is that Manifest Deployer constantly monitors the directory for changes, and thus you do not need to manually apply changes that are made to the manifest files.
Note
Each directory that is a direct descendant of
/var/lib/k0s/manifestsis considered to be its own "stack". Nested directories (further subfolders), however, are excluded from the stack mechanism and thus are not automatically deployed by the Manifest Deployer.k0s uses the independent stack mechanism for some of its internal in-cluster components, as well as for other resources. Be sure to only touch the manifests that are not managed by k0s.
Explicitly define the namespace in the manifests (Manifest Deployer does not have a default namespace).
Example
To try Manifest Deployer, create a new folder under /var/lib/k0s/manifests and then create a manifest file (such as nginx.yaml) with the following content:
apiVersion: v1
kind: Namespace
metadata:
name: nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: nginx
spec:
selector:
matchLabels:
app: nginx
replicas: 3
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80New pods will appear soon thereafter.
sudo k0s kubectl get pods --namespace nginxNAME READY STATUS RESTARTS AGE
nginx-deployment-66b6c48dd5-8zq7d 1/1 Running 0 10m
nginx-deployment-66b6c48dd5-br4jv 1/1 Running 0 10m
nginx-deployment-66b6c48dd5-sqvhb 1/1 Running 0 10mNetworking (`docs/networking.md`)
Networking
In-cluster networking
k0s supports any standard CNI network provider. For convenience, k0s does come bundled with two built-in providers, Kube-router and Calico.
Custom CNI configuration
You can opt-out of having k0s manage the network setup and choose instead to use any network plugin that adheres to the CNI specification. To do so, configure custom as the network provider in the k0s configuration file (k0s.yaml). You can deploy the CNI provider you want to use, either with Helm, plain Kubernetes manifests or any other way.
Notes
- When deploying k0s with the default settings, all pods on a node can communicate with all pods on all nodes. No configuration changes are needed to get started.
- Once you initialize the cluster with a network provider the only way to change providers is through a full cluster redeployment.
Kube-router
Kube-router is built into k0s, and so by default the distribution uses it for network provisioning. Kube-router uses the standard Linux networking stack and tools, and you can set up CNI networking without any overlays by using BGP as the main mechanism for in-cluster networking.
- Uses a bit less resources (~15%)
- Does NOT support Windows nodes
Calico
In addition to Kube-router, k0s also offers Calico as an alternative,
integrated network provider. Calico is a layer 3 container networking solution
that routes packets to pods. For example, it supports pod-specific network
policies that help secure Kubernetes clusters in demanding use cases. Calico in
k0s uses VXLAN by default. However, IP in IP is also supported via the bird
mode.
- Uses a bit more resources
- Supports Windows nodes
Controller-Worker communication
One goal of k0s is to allow for the deployment of an isolated control plane, which may prevent the establishment of an IP route between controller nodes and the pod network. Thus, to enable this communication path (which is mandated by conformance tests), k0s deploys Konnectivity service to proxy traffic from the API server (control plane) into the worker nodes. This ensures that we can always fulfill all the Kubernetes API functionalities, but still operate the control plane in total isolation from the workers.
Note: To allow Konnectivity agents running on the worker nodes to establish the connection, configure your firewalls for outbound access, port 8132. Moreover, configure your firewalls for outbound access, port 6443, in order to access Kube-API from the worker nodes.
Required ports and protocols
| Protocol | Port | Service | Direction | Notes |
|---|---|---|---|---|
| TCP | 2380 | etcd | controller ⟷ controller | |
| TCP | 6443 | kube-apiserver | worker, CLI ⟶ controller | Authenticated Kubernetes API using mTLS, ServiceAccount tokens with RBAC |
| TCP | 179 | kube-router | worker ⟷ worker | BGP routing sessions between peers |
| UDP | 4789 | calico | worker ⟷ worker | Calico VXLAN overlay |
| TCP | 10250 | kubelet | controller, worker ⟶ host * |
Authenticated kubelet API for the controller node kube-apiserver (and metrics-server add-ons) using mTLS |
| TCP | 9443 | k0s api | controller ⟷ controller | k0s controller join API, TLS with token auth |
| TCP | 8132 | konnectivity | worker ⟷ controller | Konnectivity is used as "reverse" tunnel between kube-apiserver and worker kubelets |
| TCP | 112 | keepalived | controller ⟷ controller | Only required for control plane load balancing VRRPInstances. Unless unicast is explicitly enabled, port 122 works on the ip address 224.0.0.18. 224.0.0.18 is a multicast IP address defined in RFC 3768. |
You also need enable all traffic to and from the podCIDR and serviceCIDR subnets on nodes with a worker role.
iptables
iptables can work in two distinct modes, legacy and nftables. k0s auto-detects the mode and prefers nftables. To check which mode k0s is configured with check ls -lah /var/lib/k0s/bin/. The target of the symbolic link iptables reveals the mode which k0s selected. k0s has the same logic as other k8s components, but to ensure all component have picked up the same mode you can check via:
- kube-proxy:
nsenter -t $(pidof kube-proxy) -m iptables -V - kube-router:
nsenter -t $(pidof kube-router) -m /sbin/iptables -V - calico:
nsenter -t $(pidof -s calico-node) -m iptables -V
There are known version incompatibility issues in iptables versions. k0s ships (in /var/lib/k0s/bin) a version of iptables that is tested to interoperate with all other Kubernetes components it ships with. However if you have other tooling (firewalls etc.) on your hosts that uses iptables and the host iptables version is different from the one that k0s (and other Kubernetes components) ship, it may cause networking issues. This is due to the fact that iptables, being user-space tooling, does not provide any strong version compatibility guarantees.
Firewalld & k0s
If you are using firewalld on your hosts, make sure that it runs with the same
backend (nftables or iptables) that k0s and the bundled Kubernetes
components use. Otherwise, kube-proxy, Kube-router, or Calico will program rules
in the wrong backend, which will result in a networking failure. You can
configure the active backend via the FirewallBackend option in/etc/firewalld/firewalld.conf.
Firewalld is known to be enabled by default in Oracle Linux.
Instead of adding individual ports, create dedicated firewalld services for k0s.
The k0s automated test suite uses the following XML snippets, so using them will
ensure that your configuration is aligned with our automated validation process:
Create the service file for controller nodes in/etc/firewalld/services/k0s-controller.xml as follows:
{% include "../hack/ostests/modules/os/k0s-controller.firewalld-service.xml" %}Create the service file for worker nodes in/etc/firewalld/services/k0s-worker.xml as follows:
{% include "../hack/ostests/modules/os/k0s-worker.firewalld-service.xml" %}With the services in place, enable them on the nodes. For nodes running
controller components:
$ sudo firewall-cmd --permanent --add-service=k0s-controller
successFor nodes running worker components:
$ sudo firewall-cmd --permanent --add-service=k0s-worker
success
$ sudo firewall-cmd --permanent --add-masquerade
successFor nodes that run both controller and worker components, i.e. nodes running
with the controller --enable-worker flag, you need to enable both.
Allow traffic from your pod and service networks so that the host accepts
traffic arriving via the overlay interfaces:
$ sudo firewall-cmd --permanent --add-source=<podCIDR>
success
$ sudo firewall-cmd --permanent --add-source=<serviceCIDR>
successReplace `` and <serviceCIDR> with the values configured ink0s.yaml. When you schedule workloads on controller nodes, add bothk0s-controller and k0s-worker services to the machine. Add --zone=<zone>
to the commands if you are configuring a zone other than the default.
Reload firewalld once after applying the permanent changes:
$ sudo firewall-cmd --reload
successFor automated image builds where firewalld is not yet running (for example
during cloud-init), use the offline variant:
$ sudo firewall-offline-cmd --add-service=k0s-controller
success
$ sudo firewall-offline-cmd --add-service=k0s-worker
success
$ sudo firewall-offline-cmd --add-masquerade
success
$ sudo systemctl reload firewalldAfter reloading, you should see the k0s services applied:
$ sudo firewall-cmd --list-all
public (active)
target: default
icmp-block-inversion: no
interfaces: eth0
sources: 10.244.0.0/16 10.96.0.0/12
services: cockpit dhcpv6-client k0s-controller k0s-worker ssh
ports:
protocols:
forward: yes
masquerade: yes
forward-ports:
source-ports:
icmp-blocks:
rich rules:Nllb (`docs/nllb.md`)
Node-local load balancing
For clusters that don't have an externally managed load balancer for the k0s
control plane, there is another option to get a highly available control plane,
at least from within the cluster. K0s calls this "node-local load balancing". In
contrast to an externally managed load balancer, node-local load balancing takes
place exclusively on the worker nodes. It does not contribute to making the
control plane highly available to the outside world (e.g. humans interacting
with the cluster using management tools such as Lens orkubectl), but rather makes the cluster itself internally resilient to
controller node outages.
Technical functionality
The k0s worker process manages a load balancer on each worker node's loopback
interface and configures the relevant components to use that load balancer. This
allows for requests from worker components to the control plane to be
distributed among all currently available controller nodes, rather than being
directed to the controller node that has been used to join a particular worker
into the cluster. This improves the reliability and fault tolerance of the
cluster in case a controller node becomes unhealthy.
Both Envoy and Traefik are supported as backing load balancers, with Envoy
being the default. Please note that Envoy is unavailable for ARMv7, RISC-V and
Windows. If you plan to run k0s on these platforms, use Traefik instead.
Enabling in a cluster
In order to use node-local load balancing, the cluster needs to comply with the
following:
- The cluster doesn't use an externally managed load balancer, i.e. the cluster
configuration doesn't specify a non-emptyspec.api.externalAddress. - K0s isn't running as a single node, i.e. it isn't
started using the--singleflag. - The cluster should have multiple controller nodes. Node-local load balancing
also works with a single controller node, but is only useful in conjunction
with a highly available control plane.
Add the following to the cluster configuration (k0s.yaml):
spec:
network:
nodeLocalLoadBalancing:
enabled: true
type: EnvoyProxyOr alternatively, if using k0sctl, add the following to
the k0sctl configuration (k0sctl.yaml):
spec:
k0s:
config:
spec:
network:
nodeLocalLoadBalancing:
enabled: true
type: EnvoyProxyAll newly added worker nodes will then use node-local load balancing. The k0s
worker process on worker nodes that are already running must be restarted for
the new configuration to take effect.
Full example using `k0sctl`
The following example shows a full k0sctl configuration file featuring three
controllers and two workers with node-local load balancing enabled:
apiVersion: k0sctl.k0sproject.io/v1beta1
kind: Cluster
metadata:
name: k0s-cluster
spec:
k0s:
version: {{{ k0s_version }}}
config:
spec:
network:
nodeLocalLoadBalancing:
enabled: true
type: EnvoyProxy
hosts:
- role: controller
ssh:
address: 10.81.146.254
keyPath: k0s-ssh-private-key.pem
port: 22
user: k0s
- role: controller
ssh:
address: 10.81.146.184
keyPath: k0s-ssh-private-key.pem
port: 22
user: k0s
- role: controller
ssh:
address: 10.81.146.113
keyPath: k0s-ssh-private-key.pem
port: 22
user: k0s
- role: worker
ssh:
address: 10.81.146.198
keyPath: k0s-ssh-private-key.pem
port: 22
user: k0s
- role: worker
ssh:
address: 10.81.146.51
keyPath: k0s-ssh-private-key.pem
port: 22
user: k0sSave the above configuration into a file called k0sctl.yaml and apply it in
order to bootstrap the cluster:
/* Detailed source-code truncated for AI context efficiency. */The cluster with the two nodes should be available by now. Setup the kubeconfig
file in order to interact with it:
k0sctl kubeconfig > k0s-kubeconfig
export KUBECONFIG=$(pwd)/k0s-kubeconfigThe three controllers are available and provide API Server endpoints:
$ kubectl -n kube-node-lease get \
lease/k0s-ctrl-k0s-controller-0 \
lease/k0s-ctrl-k0s-controller-1 \
lease/k0s-ctrl-k0s-controller-2 \
lease/k0s-endpoint-reconciler
NAME HOLDER AGE
k0s-ctrl-k0s-controller-0 9ec2b221890e5ed6f4cc70377bfe809fef5be541a2774dc5de81db7acb2786f1 2m37s
k0s-ctrl-k0s-controller-1 fe45284924abb1bfce674e5a9aa8d647f17c81e53bbab17cf28288f13d5e8f97 2m18s
k0s-ctrl-k0s-controller-2 5ab43278e63fc863b2a7f0fe1aab37316a6db40c5a3d8a17b9d35b5346e23b3d 2m9s
k0s-endpoint-reconciler 9ec2b221890e5ed6f4cc70377bfe809fef5be541a2774dc5de81db7acb2786f1 2m37s
$ kubectl -n default get endpoints
NAME ENDPOINTS AGE
kubernetes 10.81.146.113:6443,10.81.146.184:6443,10.81.146.254:6443 2m49sThe first controller is the current k0s leader. The two worker nodes can be
listed, too:
{% set kubelet_ver = k8s_version + '+k0s' -%}
{% set kubelet_ver_len = kubelet_ver | length -%}
$ kubectl get nodes -owide
NAME STATUS ROLES AGE {{{ 'VERSION' | ljust(kubelet_ver_len) }}} INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k0s-worker-0 Ready <none> 2m16s {{{ kubelet_ver | ljust(kubelet_ver_len) }}} 10.81.146.198 <none> Alpine Linux v3.17 5.15.83-0-virt containerd://{{{ build_var('containerd_version') }}}
k0s-worker-1 Ready <none> 2m15s {{{ kubelet_ver | ljust(kubelet_ver_len) }}} 10.81.146.51 <none> Alpine Linux v3.17 5.15.83-0-virt containerd://{{{ build_var('containerd_version') }}}There is one node-local load balancer pod running for each worker node:
$ kubectl -n kube-system get pod -owide -l app.kubernetes.io/managed-by=k0s,app.kubernetes.io/component=nllb
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nllb-k0s-worker-0 1/1 Running 0 81s 10.81.146.198 k0s-worker-0 <none> <none>
nllb-k0s-worker-1 1/1 Running 0 85s 10.81.146.51 k0s-worker-1 <none> <none>The cluster is using node-local load balancing and is able to tolerate the
outage of one controller node. Shutdown the first controller to simulate a
failure condition:
$ ssh -i k0s-ssh-private-key.pem [email protected] 'echo "Powering off $(hostname) ..." && sudo poweroff'
Powering off k0s-controller-0 ...Node-local load balancing provides high availability from within the cluster,
not from the outside. The generated kubeconfig file lists the first controller's
IP as the Kubernetes API server address by default. As this controller is gone
by now, a subsequent call to kubectl will fail:
$ kubectl get nodes
Unable to connect to the server: dial tcp 10.81.146.254:6443: connect: no route to hostChanging the server address in k0s-kubeconfig from the first controller to
another one makes the cluster accessible again. Pick one of the other controller
IP addresses and put that into the kubeconfig file. The addresses are listed
both in k0sctl.yaml as well as in the output of kubectl -n default get endpoints above.
$ ssh -i k0s-ssh-private-key.pem [email protected] hostname
k0s-controller-1
$ sed -i s#https://10\\.81\\.146\\.254:6443#https://10.81.146.184:6443#g k0s-kubeconfig
$ kubectl get nodes -owide
NAME STATUS ROLES AGE {{{ 'VERSION' | ljust(kubelet_ver_len) }}} INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k0s-worker-0 Ready <none> 3m35s {{{ kubelet_ver | ljust(kubelet_ver_len) }}} 10.81.146.198 <none> Alpine Linux v3.17 5.15.83-0-virt containerd://{{{ build_var('containerd_version') }}}
k0s-worker-1 Ready <none> 3m34s {{{ kubelet_ver | ljust(kubelet_ver_len) }}} 10.81.146.51 <none> Alpine Linux v3.17 5.15.83-0-virt containerd://{{{ build_var('containerd_version') }}}
$ kubectl -n kube-system get pods -owide -l app.kubernetes.io/managed-by=k0s,app.kubernetes.io/component=nllb
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nllb-k0s-worker-0 1/1 Running 0 2m31s 10.81.146.198 k0s-worker-0 <none> <none>
nllb-k0s-worker-1 1/1 Running 0 2m35s 10.81.146.51 k0s-worker-1 <none> <none>The first controller is no longer active. Its IP address is not listed in thedefault/kubernetes Endpoints resource and its k0s controller lease is orphaned:
$ kubectl -n default get endpoints
NAME ENDPOINTS AGE
kubernetes 10.81.146.113:6443,10.81.146.184:6443 3m56s
$ kubectl -n kube-node-lease get \
lease/k0s-ctrl-k0s-controller-0 \
lease/k0s-ctrl-k0s-controller-1 \
lease/k0s-ctrl-k0s-controller-2 \
lease/k0s-endpoint-reconciler
NAME HOLDER AGE
k0s-ctrl-k0s-controller-0 4m47s
k0s-ctrl-k0s-controller-1 fe45284924abb1bfce674e5a9aa8d647f17c81e53bbab17cf28288f13d5e8f97 4m28s
k0s-ctrl-k0s-controller-2 5ab43278e63fc863b2a7f0fe1aab37316a6db40c5a3d8a17b9d35b5346e23b3d 4m19s
k0s-endpoint-reconciler 5ab43278e63fc863b2a7f0fe1aab37316a6db40c5a3d8a17b9d35b5346e23b3d 4m47sDespite that controller being unavailable, the cluster remains operational. The
third controller has become the new k0s leader. Workloads will run just fine:
$ kubectl -n default run nginx --image=nginx
pod/nginx created
$ kubectl -n default get pods -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx 1/1 Running 0 16s 10.244.0.5 k0s-worker-1 <none> <none>
$ kubectl -n default logs nginx
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
[notice] 1#1: using the "epoll" event method
[notice] 1#1: nginx/1.23.3
[notice] 1#1: built by gcc 10.2.1 20210110 (Debian 10.2.1-6)
[notice] 1#1: OS: Linux 5.15.83-0-virt
[notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
[notice] 1#1: start worker processes
[notice] 1#1: start worker process 28Podsecurity (`docs/podsecurity.md`)
Pod Security Standards
Since Pod Security Policies have been removed in Kubernetes v1.25, Kubernetes
offers Pod Security Standards – a new way to enhance cluster security.
To enable Pod Security Standards in k0s, create an admission controller
configuration file:
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1beta1
kind: PodSecurityConfiguration
# Defaults applied when a mode label is not set.
defaults:
enforce: "privileged"
enforce-version: "latest"
exemptions:
# Don't forget to exempt namespaces or users that are responsible for deploying
# cluster components, because they need to run privileged containers
usernames: ["admin"]
namespaces: ["kube-system"]
```Add these extra arguments to the k0s configuration:
```yaml
apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig
spec:
api:
extraArgs:
admission-control-config-file: /path/to/admission/control/config.yaml
```Raspberry Pi4 (`docs/raspberry-pi4.md`)
Create a Raspberry Pi 4 cluster
Prerequisites
This guide assumes that you use a Raspberry Pi 4 Model B computer and a
sufficiently large SD card of at least 32 GB. We will be using Ubuntu Linux
for this guide, although k0s should run quite fine on other 64-bit Linux
distributions for the Raspberry Pi as well. Please file a Bug if you encounter
any obstacles.
Set up the system
Prepare SD card and boot up the Raspberry Pi
Install Ubuntu Server 22.04.1 LTS 64-bit for Raspberry Pi. Ubuntu
provides a step by step guide for the installation process. They
use Raspberry Pi Imager, a specialized imaging utility that you can use to
write the Ubuntu image, amongst others, to your SD cards. Follow that
guide to get a working installation. (You can skip part 5 of the
guide, since we won't need a Desktop Environment to run k0s.)
Alternatively, you can also opt to download the Ubuntu server image
for Raspberry Pi manually and write it to an SD card using a tool like dd:
wget https://cdimage.ubuntu.com/releases/22.04.1/release/ubuntu-22.04.1-preinstalled-server-arm64+raspi.img.xz
unxz ubuntu-22.04.1-preinstalled-server-arm64+raspi.img.xz
dd if=ubuntu-22.04.1-preinstalled-server-arm64+raspi.img of=/dev/mmcblk0 bs=4M status=progressNote: The manual process is more prone to accidental data loss than the
guided one via Raspberry Pi Imager. Be sure to choose the correct device names.
The previous content of the SD card will be wiped. Moreover, the partition
written to the SD card needs to be resized to make the full capacity of the card
available to Ubuntu. This can be achieved, for example, in this way:
growpart /dev/mmcblk0 2
resize2fs /dev/mmcblk0p2Ubuntu uses cloud-init to allow for automated customizations of the system
configuration. The cloud-init configuration files are located on the boot
partition of the SD card. You can mount that partition and modify those, e.g. to
provision network configuration, users, authorized SSH keys, additional packages
and also an automatic installation of k0s.
After you have prepared the SD card, plug it into the Raspberry Pi and boot it
up. Once cloud-init finished bootstrapping the system, the default login
credentials are set to user ubuntu with password ubuntu (which you will be
prompted to change on first login).
Review network configurations
Note: For network configuration purposes, this documentation assumes that all
of your computers are connected on the same subnet.
Review k0s's required ports and protocols to ensure that your network and
firewall configurations allow necessary traffic for the cluster.
Review the Ubuntu Server Networking Configuration documentation to ensure that
all systems have a static IP address on the network, or that the network is
providing a static DHCP lease for the nodes. If the network should be managed
via cloud-init, please refer to their documentation.
(Optional) Provision SSH keys
Ubuntu Server deploys and enables OpenSSH via
cloud-init by default. Confirm, though, that for whichever user you will deploy
the cluster with on the build system, their SSH Key is copied to each node's
root user. Before you start, the configuration should be such
that the current user can run:
ssh root@${HOST}Where ${HOST} is any node and the login can succeed with no further prompts.
(Optional) Create a swap file
While having a swap file is technically optional, it can help to ease memory
pressure when running memory intensive workloads or on Raspberry Pis with less
than 8 GB of RAM.
To create a swap file:
shellfallocate -l 2G /swapfile && \ chmod 0600 /swapfile && \ mkswap /swapfile && \ swapon -aEnsure that the usage of swap is not too aggressive by setting the
sudo sysctl vm.swappiness=10(the default is generally higher) and configuring it
to be persistent in/etc/sysctl.d/*.Ensure that your swap is mounted after reboots by confirming that the
following line exists in your/etc/fstabconfiguration:shell/swapfile none swap sw 0 0
Download k0s
Download a k0s release. For
example:
wget -O /tmp/k0s https://github.com/k0sproject/k0s/releases/download/{{{ k0s_version }}}/k0s-{{{ k0s_version }}}-arm64 # replace version number!
sudo install /tmp/k0s /usr/local/bin/k0s― or ―
Use the k0s download script (as one command) to download the latest stable k0s
and make it executable in /usr/bin/k0s.
curl --proto '=https' --tlsv1.2 -sSf https://get.k0s.sh | sudo shAt this point you can run k0s:
ubuntu@ubuntu:~$ k0s version
{{{ k0s_version }}}To check if k0s's system requirements and external
runtime dependencies are fulfilled by your current
setup, you can invoke k0s sysinfo:
/* Detailed source-code truncated for AI context efficiency. */Deploy a node
Each node can now serve as a control plane node or worker node or both.
As single node
This is a self-contained single node setup which runs both control plane
components and worker components. If you don't plan join any more nodes into the
cluster, this is for you.
Install the k0scontroller service:
ubuntu@ubuntu:~$ sudo k0s install controller --single
ubuntu@ubuntu:~$ sudo systemctl status k0scontroller.service
○ k0scontroller.service - k0s - Zero Friction Kubernetes
Loaded: loaded (/etc/systemd/system/k0scontroller.service; enabled; vendor preset: enabled)
Active: inactive (dead)
Docs: https://docs.k0sproject.ioStart it:
/* Detailed source-code truncated for AI context efficiency. */{% set kubelet_ver = k8s_version + '+k0s' -%}
{% set kubelet_ver_len = kubelet_ver | length -%}
When the cluster is up, try to have a look:
ubuntu@ubuntu:~$ sudo k0s kc get nodes -owide
NAME STATUS ROLES AGE {{{ 'VERSION' | ljust(kubelet_ver_len) }}} INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
ubuntu Ready control-plane 4m41s {{{ kubelet_ver | ljust(kubelet_ver_len) }}} 10.152.56.54 <none> Ubuntu 22.04.1 LTS 5.15.0-1013-raspi containerd://{{{ build_var('containerd_version') }}}
ubuntu@ubuntu:~$ sudo k0s kc get pod -owide -A
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kube-system kube-proxy-kkv2l 1/1 Running 0 4m44s 10.152.56.54 ubuntu <none> <none>
kube-system kube-router-vf2pv 1/1 Running 0 4m44s 10.152.56.54 ubuntu <none> <none>
kube-system coredns-88b745646-wd4mp 1/1 Running 0 5m10s 10.244.0.2 ubuntu <none> <none>
kube-system metrics-server-7d7c4887f4-ssk49 1/1 Running 0 5m6s 10.244.0.3 ubuntu <none> <none>Overall, the single k0s node uses less than 1 GiB of RAM:
ubuntu@ubuntu:~$ free -h
total used free shared buff/cache available
Mem: 3.7Gi 715Mi 1.3Gi 3.0Mi 1.7Gi 2.8Gi
Swap: 0B 0B 0BAs a controller node
This will install k0s as a single non-HA controller. It won't be able to run any
workloads, so you need to connect more workers to it.
Install the k0scontroller service. Note that we're not specifying any flags:
ubuntu@ubuntu:~$ sudo k0s install controller
ubuntu@ubuntu:~$ systemctl status k0scontroller.service
○ k0scontroller.service - k0s - Zero Friction Kubernetes
Loaded: loaded (/etc/systemd/system/k0scontroller.service; enabled; vendor preset: enabled)
Active: inactive (dead)
Docs: https://docs.k0sproject.ioStart it:
/* Detailed source-code truncated for AI context efficiency. */As soon as the controller is up, we can try to inspect the API as we did for the
single node:
ubuntu@ubuntu:~$ sudo k0s kc get nodes -owide
No resources found
ubuntu@ubuntu:~$ sudo k0s kc get pod -owide -A
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kube-system coredns-88b745646-6tpwm 0/1 Pending 0 29s <none> <none> <none> <none>
kube-system metrics-server-7d7c4887f4-9k5k5 0/1 Pending 0 24s <none> <none> <none> <none>As we see, there are no nodes and two pending pods. A control plane without
workers. The memory consumption is below the single node controller, but not
much:
ubuntu@ubuntu:~$ free -h
total used free shared buff/cache available
Mem: 3.7Gi 678Mi 2.3Gi 3.0Mi 758Mi 2.9Gi
Swap: 0B 0B 0BThis controller runs a full-fledged control plane, backed by etcd, as opposed
to the lightweight kine based one from the single node example. For the
latter, k0s doesn't support joining new nodes.
More nodes can be added by creating join tokens. To add a worker node, create a
token for it:
/* Detailed source-code truncated for AI context efficiency. */Save the join token for subsequent steps.
As a worker node
To join an existing k0s cluster, create the join token file for the worker
(where $TOKEN_CONTENT is one of the join tokens created in the control plane
setup):
sudo sh -c 'mkdir -p /var/lib/k0s/ && umask 077 && echo "$TOKEN_CONTENT" > /var/lib/k0s/join-token'After that, install the k0sworker service:
ubuntu@ubuntu:~$ sudo k0s install worker --token-file /var/lib/k0s/join-token
ubuntu@ubuntu:~$ systemctl status k0sworker.service
○ k0sworker.service - k0s - Zero Friction Kubernetes
Loaded: loaded (/etc/systemd/system/k0sworker.service; enabled; vendor preset: enabled)
Active: inactive (dead)
Docs: https://docs.k0sproject.ioStart the service:
/* Detailed source-code truncated for AI context efficiency. */As this is a worker node, we cannot access the Kubernetes API via the builtink0s kc subcommand, but we can check the k0s API instead:
ubuntu@ubuntu:~$ sudo k0s status
Version: {{{ k0s_version }}}
Process ID: 1631
Role: worker
Workloads: true
SingleNode: falseThe memory requirements are also pretty low:
ubuntu@ubuntu:~$ free -h
total used free shared buff/cache available
Mem: 3.7Gi 336Mi 2.1Gi 3.0Mi 1.2Gi 3.2Gi
Swap: 0B 0B 0BConnect to the cluster
On a controller node, generate a new raspi-cluster-master user with admin
rights and get a kubeconfig for it:
/* Detailed source-code truncated for AI context efficiency. */Using the above kubeconfig, you can now access and use the cluster:
/* Detailed source-code truncated for AI context efficiency. */Raspberry Pi5 (`docs/raspberry-pi5.md`)
Create a Raspberry Pi 5 cluster
Prerequisites
This guide assumes that you have a Raspberry Pi 5 and a
sufficiently large SD card of at least 32 GB. We will be using Raspberry Pi OS
for this guide, though k0s should run just fine on other 64-bit Linux
distributions for the Raspberry Pi 5 as well. Please file a Bug if you encounter
any issues.
Set up the system
Follow the official Raspberry Pi instructions on booting Pi OS from the USB drive or another method depending on your needs.
SSH Access
Typically, you can enable SSH while creating the bootable drive for Raspberry Pi OS. If not, you can enable it later
Add your public key to the server.
From your host machine, run ssh-copy-id to copy your public SSH key to your Pi 5:bashssh-copy-id -i ~/.ssh/id_rsa.pub <YOUR_USER_NAME>@<IP_ADDRESS_OF_THE_SERVER>When prompted, enter the password for your user account for the Pi. Your public key should be copied at the appropriate folder on the remote Pi automatically.
Note:
~/.ssh/id_rsa.pubis the default location for the public ssh key. If your key is elsewhere, adjust accordingly.Verify SSH access
shellssh <YOUR_USER_NAME>@<IP_ADDRESS_OF_THE_SERVER>If your key has a paraphrase, you’ll be prompted for it.
Enable the memory cgroup controller
Raspberry Pi OS does not enable the memory cgroup controller by default. However, it is required to run containerized workloads, so enable it:
Edit /boot/cmdline.txt:
bashsudo nano /boot/cmdline.txtAppend (on the same single line):
bashcgroup_enable=memory cgroup_memory=1Reboot
bashsudo reboot
Install k0s
Download k0s
Download a k0s release. For
example:
wget -O /tmp/k0s https://github.com/k0sproject/k0s/releases/download/{{{ k0s_version }}}/k0s-{{{ k0s_version }}}-arm64 # replace version number!
sudo install /tmp/k0s /usr/local/bin/k0s― or ―
Use the k0s download script (as one command) to download the latest stable k0s
and make it executable in /usr/bin/k0s.
curl --proto '=https' --tlsv1.2 -sSf https://get.k0s.sh | sudo shAt this point you can run k0s:
$ sudo k0s version
{{{ k0s_version }}}To check if k0s's system requirements and external
runtime dependencies are fulfilled by your current
setup, you can invoke k0s sysinfo:
/* Detailed source-code truncated for AI context efficiency. */Deploy a Node using k0s
If you want a more hands-on process for setting up your Pi 5 devices as Kubernetes nodes with the k0s binary, refer to the guide for Pi devices.
Deploy a Node using k0sctl
Install k0sctl on Your Host
Follow the k0sctl installation guide and install k0sctl on your host machine.
Single node K0s cluster
For this example, we'll create a cluster.yaml that describes your known Pi 5 device and use it as a single node (controller & worker) cluster, for example:
apiVersion: k0sctl.k0sproject.io/v1beta1
kind: Cluster
metadata:
name: k0s-cluster
user: admin
spec:
hosts:
- ssh:
address: <IP_ADDRESS_OF_THE_SERVER>
user: <YOUR_USER_NAME>
port: 22
keyPath: ~/.ssh/id_rsa
role: controller+workerSSH agent
By default, k0sctl doesn’t prompt you for passphrases, so the easiest solution is to load your key into an SSH agent before running k0sctl. Here’s how you can do it:
Start the SSH agent (if not already running)
basheval "$(ssh-agent -s)"Add your private key (you’ll be prompted for the passphrase)
bashssh-add ~/.ssh/id_rsaVerify the key is loaded
bashssh-add -l
Deploy cluster
Apply the cluster.yaml using k0sctl on your local machine.
text
/* Detailed source-code truncated for AI context efficiency. */
2. Fetch the kubeconfig use k0sctl.
```bash
k0sctl kubeconfig --config cluster.yaml > pi_cluster.kubeconfig
```
3. Export KUBECONFIG and verify
```console
$ export KUBECONFIG=pi_cluster.kubeconfig
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
pi Ready control-plane 2m54s {{{ k8s_version }}}+k0s
```
## Tear down k0s on Pi 5
If you need to remove k0s entirely (for example, if you run into conflicts or just want a clean slate):
1. Stop existing processes
```bash
sudo k0s stop
```
2. Reset k0s
```bash
sudo k0s reset
```
3. Remove k0s binaries
```bash
sudo rm -rf /usr/local/bin/k0s
```
4. Reboot Pi 5.
```bash
sudo reboot
```
---
### Reinstall K0sctl (`docs/reinstall-k0sctl.md`)
# Reinstall a node
`k0sctl` currently does not support changing all the configuration of containerd (`state`, `root`) on the fly.
For example, in order to move containerd's `root` directory to a new partition/drive, you have to provide `--data-dir /new/drive` in your k0sctl `installFlags` for each (worker) node. `--data-dir` is an option of `k0s` and then added to the service unit.
The following is an example of that:
```yaml
# spec.hosts[*].installFlags
- role: worker
installFlags:
- --profile flatcar
- --enable-cloud-provider
- --data-dir /new/drive
- --kubelet-extra-args="--cloud-provider=external"However, the installFlags are only used when the node is installed.
Steps
Drain the node:
kubectl drain node.hostnameAccess your node (e.g. via ssh) to stop and reset k0s:
sudo k0s stop
sudo k0s resetReboot the node (for good measure):
sudo systemctl rebootOnce the node is available again, run k0sctl apply to integrate it into your cluster and uncordon the node to allow pods to be scheduled:
k0sctl apply -c config.yaml
kubectl uncordon node.hostnameReleases (`docs/releases.md`)
Releases
This page describes how we release and support the k0s project. Mirantis Inc. can also provide commercial support for k0s.
Upstream Kubernetes release & support cycle
{% set version_parts = k0s_version.split('.', maxsplit=2) -%}
{% set k0s_minor = version_parts[0] + '.' + version_parts[1] -%}
{% set k0s_prev_minor = version_parts[0] + '.' ~ ((version_parts[1] | int) - 1) -%}
This release and support cycle is followed
for ALL new minor releases. A minor release can be e.g. {{{ k0s_prev_minor }}},
{{{ k0s_minor }}} and so on. What this means in practice is that every 4 months
there is a new minor release published.
After a minor release is published, the upstream community is maintaining it for 14 months. Maintenance in this case means that upstream Kubernetes provides bug fixes, CVE mitigations and such for 14 months per minor release.
k0s release and support model
Starting from the k0s 1.21, k0s started following the Kubernetes project's release and support model.
k0s project follows closely the upstream Kubernetes release cycle. The only difference to upstream Kubernetes release / maintenance schedule is that our initial release date is always a few weeks behind the upstream Kubernetes version release date as we are building our version of k0s from the officially released version of Kubernetes and need time for testing the final version before shipping.
Given the fact that upstream Kubernetes provides support and patch releases for a minor version for roughly 14 months, it means that k0s will follow this same model. Each minor release is maintained for roughly 14 months since its initial release.
k0s project will typically include patches and fixes included in a Kubernetes
upstream patch release for the fixes needed in k0s own codebase. For example, if
a bug is identified in {{{ k0s_minor }}} series k0s project will create and ship
a fix for it with the next upstream Kubernetes {{{ k0s_minor }}}.x release. In
rare cases where a critical bug is identified we may also ship "out of band"
patches. Such out-of-band release would be identified in the version string
suffix. For example a normal release following Kubernetes upstream would be {{{
k0s_minor }}}.0+k0s.0 whereas a critical out-of-band patch would be identified as
{{{ k0s_minor }}}.0+k0s.1.
New features and enhancements
The biggest new k0s features will typically only be delivered on top of the latest Kubernetes version, but smaller enhancements can be included in older release tracks as well.
Version string
{% set version_parts = k0s_version.split('+', maxsplit=1) -%}
The k0s version string consists of the Kubernetes version and the k0s version. For example:
{{{ version_parts | join('+') }}}The Kubernetes version ({{{ version_parts[0] }}}) is the first part, and the
last part ({{{ version_parts[1] }}}) reflects the k0s version, which is built
on top of the certain Kubernetes version.
Remove Controller (`docs/remove_controller.md`)
Remove or replace a controller
You can manually remove or replace a controller from a multi-node k0s cluster (>=3 controllers) without downtime.
However, you have to maintain quorum on Etcd while doing so.
Remove a controller
If your controller is also a worker (k0s controller --enable-worker), you first have to delete the controller from Kubernetes itself.
To do so, run the following commands from the controller:
# Remove the containers from the node and cordon it
k0s kubectl drain --ignore-daemonsets --delete-emptydir-data <controller>
# Delete the node from the cluster
k0s kubectl delete node <controller>Delete Autopilot's ControlNode object for the controller node:
k0s kubectl delete controlnode.autopilot.k0sproject.io <controller>Then you need to remove it from the Etcd cluster.
For example, if you want to remove controller01 from a cluster with 3 controllers:
# First, list the Etcd members
k0s etcd member-list
{"members":{"controller01":"<PEER_ADDRESS1>", "controller02": "<PEER_ADDRESS2>", "controller03": "<PEER_ADDRESS3>"}}
# Then, remove the controller01 using its peer address
k0s etcd leave --peer-address "<PEER_ADDRESS1>"The controller is now removed from the cluster.
To reset k0s on the machine, run the following commands:
k0s stop
k0s reset
rebootDeclarative Etcd member management
Starting from version 1.30, k0s also supports a declarative way to remove an
etcd member. Since in k0s the etcd cluster is set up so that the etcd API is
NOT exposed outside the nodes, it makes it difficult for external automation
like Cluster API, Terraform, etc. to handle controller node replacements.
Each controller manages their own EtcdMember object.
k0s kubectl get etcdmember
NAME PEER ADDRESS MEMBER ID JOINED RECONCILE STATUS
controller0 172.17.0.2 b8e14bda2255bc24 True
controller1 172.17.0.3 cb242476916c8a58 True
controller2 172.17.0.4 9c90504b1bc867bb TrueBy marking an EtcdMember object to leave the etcd cluster, k0s will handle the
interaction with etcd. For example, in a 3 controller HA setup, you can
remove a member by flagging it to leave:
$ kubectl patch etcdmember controller2 -p '{"spec":{"leave":true}}' --type merge
etcdmember.etcd.k0sproject.io/controller2 patchedThe join/leave status is tracked in the object's conditions. This allows you to
wait for the leave to actually happen:
$ kubectl wait etcdmember controller2 --for condition=Joined=False
etcdmember.etcd.k0sproject.io/controller2 condition metYou'll see the node left etcd cluster:
$ k0s kc get etcdmember
NAME PEER ADDRESS MEMBER ID JOINED RECONCILE STATUS
controller0 172.17.0.2 b8e14bda2255bc24 True
controller1 172.17.0.3 cb242476916c8a58 True
controller2 172.17.0.4 9c90504b1bc867bb False Success$ k0s etcd member-list
{"members":{"controller0":"https://172.17.0.2:2380","controller1":"https://172.17.0.3:2380"}}The objects for members that have already left the etcd cluster are kept
available for tracking purposes. Once the member has left the cluster, the
object status will reflect that it is safe to remove it.
Note: If you re-join same node without removing the corresponding etcdmember object the desired state will be updated back to spec.leave: false automatically. This is since currently in k0s there's no easy way to prevent a node joining etcd cluster.
Replace a controller
To replace a controller, you first remove the old controller (like described above) then follow the manual installation procedure to add the new one.
Requirements (`docs/requirements.txt`)
babel==2.18.0
backrefs==8.1
bracex==3.1
certifi==2026.7.22
charset-normalizer==3.4.7
click==8.5.0
colorama==0.4.6
ghp-import==2.1.0
hjson==3.1.0
idna==3.20
Jinja2==3.1.6
Markdown==3.11
MarkupSafe==3.0.4
mdx-truly-sane-lists==1.3
mergedeep==1.3.4
mike==2.2.0
mkdocs==1.6.1
mkdocs-exclude==1.0.2
mkdocs-get-deps==0.2.2
mkdocs-include-markdown-plugin==7.3.0
mkdocs-macros-plugin==1.5.0
mkdocs-material==9.7.7
mkdocs-material-extensions==1.3.1
packaging==26.3
paginate==0.5.7
pathspec==1.1.1
platformdirs==4.13.0
Pygments==2.21.0
pymdown-extensions==12.2
pyparsing==3.3.3
python-dateutil==2.9.0.post0
PyYAML==6.0.3
pyyaml_env_tag==1.1
requests==2.34.2
six==1.17.0
super_collections==0.6.2
termcolor==3.3.0
urllib3==2.7.0
verspec==0.1.0
watchdog==6.0.0
wcmatch==11.1
Reset (`docs/reset.md`)
Reset (Uninstall)
Resetting k0s nodes essentially reverts them to a pre-k0s state. This operation
is particularly useful in scenarios where you need to cleanly remove k0s from
nodes, either for troubleshooting purposes or as a precursor to a fresh
installation. It is designed to remove all components, configurations, and data
associated with k0s from a node. This includes, but is not limited to, the
following:
- Processes and containers: Terminates all running k0s processes to ensure that
there are no active components left. This includes all container processes
managed by the Container Runtime. - Mounts under k0s data directory: In order to prevent persistent data to be
deleted, all mount points under k0s' data directory will be unmounted. If an
unmount fails, it will be unmounted lazy. - Data stored on the node: Deletes the whole k0s data directory, which includes
- all k0s-related configuration files, including those used for cluster setup
and node-specific settings, - all data stores managed by k0s, including etcd or kine data for cluster
state and any temporary files created during operation, - and all certificates and other key material generated by k0s for node
authentication and communication within the cluster.
- all k0s-related configuration files, including those used for cluster setup
- Network settings: Reverts any network configurations made by k0s, such as
network interfaces or iptables rules set up specifically for cluster
communication. This is done on a best effort basis. It's recommended that you
reboot the host after a reset to ensure that there are no k0s remnants in the
host's network configuration. Custom CNI plugins are not cleaned up. - Registration with the host's init system: Reverts the registration done by
k0s install. After a reset, k0s won't be automatically started when the
host boots.
After a successful reset, the k0s binary itself remains. It can then be used to
join another cluster or create a new one.
It's important to understand that performing a reset is a destructive operation
that will result in the loss of all data and configurations related to k0s on
the affected nodes. Therefore, it should be performed with care, ideally after
ensuring that all necessary data has been backed up or that the cluster can be
safely disassembled without data loss.
K0s can be reset locally on a host using the k0s reset command. Whole clusters
can be reset remotely with the k0sctl reset command.
Reset a k0s node locally
To ensure operational safety, k0s reset includes a safeguard that prevents it
from being executed while k0s is running, so it must be stopped first:
Stop the service:
consolesudo k0s stopInvoke the
resetcommand:console$ sudo k0s reset WARN[2024-03-28 09:15:36] To ensure a full reset, a node reboot is recommended.
Reset a k0s cluster remotely using k0sctl
K0sctl can be used to connect and reset all cluster nodes in a single command.
Invoke
k0sctl resetcommand:console$ k0sctl reset --config k0sctl.yaml k0sctl v0.21.0 Copyright 2023, k0sctl authors. ? Going to reset all of the hosts, which will destroy all configuration and data, Are you sure? Yes INFO ==> Running phase: Connect to hosts INFO [ssh] 13.53.43.63:22: connected INFO [ssh] 13.53.218.149:22: connected INFO ==> Running phase: Detect host operating systems INFO [ssh] 13.53.43.63:22: is running Ubuntu 22.04.4 LTS INFO [ssh] 13.53.218.149:22: is running Ubuntu 22.04.4 LTS INFO ==> Running phase: Acquire exclusive host lock INFO ==> Running phase: Prepare hosts INFO ==> Running phase: Gather k0s facts INFO [ssh] 13.53.43.63:22: found existing configuration INFO [ssh] 13.53.43.63:22: is running k0s controller version {{{ k0s_version }}} INFO [ssh] 13.53.218.149:22: is running k0s worker version {{{ k0s_version }}} INFO [ssh] 13.53.43.63:22: checking if worker has joined INFO ==> Running phase: Reset workers INFO [ssh] 13.53.218.149:22: reset INFO ==> Running phase: Reset controllers INFO [ssh] 13.53.43.63:22: reset INFO ==> Running phase: Reset leader INFO [ssh] 13.53.43.63:22: reset INFO ==> Running phase: Release exclusive host lock INFO ==> Running phase: Disconnect from hosts INFO ==> Finished in 8s
Runtime (`docs/runtime.md`)
Runtime
k0s supports any container runtime that implements the CRI specification.
k0s comes bundled with containerd as the default Container Runtime Interface (CRI) and runc as the default low-level runtime. In most cases they don't require any configuration changes. However, if custom configuration is needed, this page provides some examples.
containerd configuration
By default k0s manages the full containerd configuration. You have the option of fully overriding, and thus also managing, the configuration yourself.
User managed containerd configuration
In the default k0s generated configuration there's a "magic" comment telling containerd it is managed by k0s:
# k0s_managed=trueIf you wish to take over the configuration management, remove this line.
To make changes to the containerd configuration you must first generate a default containerd configuration, with the default values set to /etc/k0s/containerd.toml:
containerd config default > /etc/k0s/containerd.tomlk0s runs containerd with the following default values:
/var/lib/k0s/bin/containerd \
--root=/var/lib/k0s/containerd \
--state=/run/k0s/containerd \
--address=/run/k0s/containerd.sock \
--config=/etc/k0s/containerd.tomlNext, add the following default values to the configuration file:
version = 3
root = "/var/lib/k0s/containerd"
state = "/run/k0s/containerd"
...
[grpc]
address = "/run/k0s/containerd.sock"k0s managed dynamic runtime configuration
As of 1.27.1, k0s allows dynamic configuration of containerd CRI runtimes. This
works by k0s creating a special directory in /etc/k0s/containerd.d/, where
users can place partial containerd configuration TOML files (i.e. files with a.toml extension). k0s will automatically pick up these files and add them as containerd
configuration imports. When k0s sees changes in this directory, it will automatically reload containerd with the new configuration.
Starting with k0s 1.36, k0s uses containerd 2 which modifies the configuration
and all drop-in files must use the containerd v3 configuration format. Either
omit the version field entirely or set it to version = 3. Any file that setsversion = 2 or that references the legacy io.containerd.grpc.v1.cri plugin
path will cause k0s to exit with an error on pre-flight checks. If you have drop-in files from previous versions,
update them before restarting k0s.
Please note, that in order for drop-ins in /etc/k0s/containerd.d to take effect on running configuration, /etc/k0s/containerd.toml needs to be k0s managed.
If you change the first magic line (# k0s_managed=true) in the /etc/k0s/containerd.toml (by accident or on purpose), it automatically becomes "not k0s managed". To make it "k0s managed" again, remove /etc/k0s/containerd.toml and restart k0s service on the node, it'll be recreated by k0s.
Examples
The following sections provide some examples of how to use k0s managed drop-in
configurations for common configuration tasks.
Configuring OCI registry mirrors
Say you host an internal registry mirror of Docker Hub asdocker-mirror.internal.acme.corp. Then you could use the following drop-in
configuration:
/etc/k0s/containerd.d/cri-registry.toml:
version = 3
[plugins."io.containerd.cri.v1.images".registry]
config_path = "/etc/k0s/containerd.d/certs.d"Create the file /etc/k0s/containerd.d/certs.d/docker.io/hosts.toml with the following content:
# Always resolve tags against Docker Hub as the "server"
server = "https://registry-1.docker.io"
[host."https://docker-mirror.internal.acme.corp:5000"]
capabilities = ["pull", "resolve"]
# Trust the internal mirror's certificate. Paths will be resolved relative to
# the containing file's directory, e.g. the below path will be resolved to
# /etc/k0s/containerd.d/certs.d/docker.io/internal-mirror.crt
ca = "internal-mirror.crt"
# Or, alternatively, disable TLS verification, if you know what you're doing:
# skip_verify = true
# Use Google's public pull-through cache
[host."https://mirror.gcr.io"]
capabilities = ["pull", "resolve"]For more details on how to configure registry hosts, please refer to the
official containerd configuration.
Using gVisor
gVisor is an application kernel, written in Go, that implements a substantial portion of the Linux system call interface. It provides an additional layer of isolation between running applications and the host operating system.
Install the needed gVisor binaries into the host.
shell( set -e ARCH=$(uname -m) URL=https://storage.googleapis.com/gvisor/releases/release/latest/${ARCH} wget ${URL}/runsc ${URL}/runsc.sha512 \ ${URL}/containerd-shim-runsc-v1 ${URL}/containerd-shim-runsc-v1.sha512 sha512sum -c runsc.sha512 \ -c containerd-shim-runsc-v1.sha512 rm -f *.sha512 chmod a+rx runsc containerd-shim-runsc-v1 sudo mv runsc containerd-shim-runsc-v1 /usr/local/bin )Refer to the gVisor install docs for more information.
Prepare the config for
k0smanaged containerd, to utilize gVisor as additional runtime:shellcat <<EOF | sudo tee /etc/k0s/containerd.d/gvisor.toml version = 3 [plugins."io.containerd.cri.v1.runtime".containerd.runtimes.runsc] runtime_type = "io.containerd.runsc.v1" EOFStart and join the worker into the cluster, as normal:
shellk0s worker $tokenRegister containerd to the Kubernetes side to make gVisor runtime usable for workloads (by default, containerd uses normal runc as the runtime):
shellcat <<EOF | kubectl apply -f - apiVersion: node.k8s.io/v1 kind: RuntimeClass metadata: name: gvisor handler: runsc EOFAt this point, you can use gVisor runtime for your workloads:
yamlapiVersion: v1 kind: Pod metadata: name: nginx-gvisor spec: runtimeClassName: gvisor containers: - name: nginx image: nginx(Optional) Verify that the created nginx pod is running under gVisor runtime:
shell# kubectl exec nginx-gvisor -- dmesg | grep -i gvisor [ 0.000000] Starting gVisor...
Using nvidia-container-runtime
First, deploy the NVIDIA GPU operator Helm chart with the following commands on top of your k0s cluster:
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update
helm install nvidia-gpu-operator -n nvidia-gpu-operator \
--create-namespace \
--set operator.defaultRuntime=containerd \
--set toolkit.env[0].name=CONTAINERD_CONFIG \
--set toolkit.env[0].value=/etc/k0s/containerd.d/nvidia.toml \
--set toolkit.env[1].name=CONTAINERD_SOCKET \
--set toolkit.env[1].value=/run/k0s/containerd.sock \
--set toolkit.env[2].name=CONTAINERD_RUNTIME_CLASS \
--set toolkit.env[2].value=nvidia \
nvidia/gpu-operatorWith this Helm chart values, NVIDIA GPU operator will deploy both driver and toolkit to the GPU nodes and additionally will configure containerd with NVIDIA specific runtime.
Note: Detailed installation instructions can be found in the official
NVIDIA GPU Operator documentation.
Using custom CRI runtimes
Warning: You can use your own CRI runtime with k0s (for example, docker). However, k0s will not start or manage the runtime, and configuration is solely your responsibility.
Use the option --cri-socket to run a k0s worker with a custom CRI runtime. The option takes input in the form of <type>: (the only supported type is remote).
Using Docker as the container runtime
As of Kubernetes 1.24, the use of Docker as a container runtime is no longer
supported out of the box. However, Mirantis provides
cri-dockerd, a shim that allows Docker to be controlled via CRI. It's based
on the dockershim that was previously part of upstream
Kubernetes.
Configuration
In order to use Docker as the container runtime for k0s, the following steps
need to be taken:
Manually install required components.
On eachk0s workerandk0s controller --enable-workernode, both
Docker Engine and cri-dockerd need to be installed manually. Follow the
official Docker Engine installation guide and cri-dockerd
installation instructions.Configure and restart affected k0s nodes.
Once installations are complete, the nodes needs to be restarted with the
--cri-socketflag pointing to cri-dockerd's socket, which is typically
located at/var/run/cri-dockerd.sock. For instance, the commands to start a
node would be as follows:k0s worker --cri-socket=remote:unix:///var/run/cri-dockerd.sock
or, respectively
k0s controller --enable-worker --cri-socket=remote:unix:///var/run/cri-dockerd.sock When running k0s as a service, consider reinstalling the
service with the appropriate flags:
sudo k0s install --force worker --cri-socket=remote:unix:///var/run/cri-dockerd.sockor, respectively
sudo k0s install --force controller --enable-worker --cri-socket=remote:unix:///var/run/cri-dockerd.sockIn scenarios where Docker is managed via systemd, it is crucial that thecgroupDriver: systemd setting is included in the Kubelet configuration. It can
be added to the workerProfiles section of the k0s configuration. An example of
how the k0s configuration might look:
apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig
metadata:
name: k0s
spec:
workerProfiles:
- name: systemd-docker-cri
values:
cgroupDriver: systemdNote that this is a cluster-wide configuration setting that must be added to
the k0s controller's configuration rather than directly to the workers, or to
the cluster configuration if using dynamic configuration. See the worker
profiles section of the documentation for more details. When starting workers,
both the --profile=systemd-docker-cri and --cri-socket flags are required.
The profile name, such as systemd-docker-cri, is flexible. Alternatively,
this setting can be applied to the default profile, which will apply to all
nodes started without a specific profile. In this case, the --profile flag is
not needed.
Please note that there are currently some pitfalls around container
metrics when using cri-dockerd.
Verification
The successful configuration can be verified by executing the following command:
{% set kubelet_ver = k8s_version + '+k0s' -%}
{% set kubelet_ver_len = kubelet_ver | length -%}
$ kubectl get nodes -o wide
NAME STATUS ROLES AGE {{{ 'VERSION' | ljust(kubelet_ver_len) }}} INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
docker-worker-0 Ready <none> 15m {{{ kubelet_ver | ljust(kubelet_ver_len) }}} 172.27.77.155 <none> Ubuntu 22.04.3 LTS 5.15.0-82-generic docker://24.0.7On the worker nodes, the Kubernetes containers should be listed as regular
Docker containers:
/* Detailed source-code truncated for AI context efficiency. */Security (`docs/security.md`)
Security policy
For the security policy, please refer to the upstream k0s documentation.
Selinux (`docs/selinux.md`)
SELinux Overview
SELinux enforces mandatory access control policies that confine user programs and system services, as well as access to files and network resources. Limiting privilege to the minimum required to work reduces or eliminates the ability of these programs and daemons to cause harm if faulty or compromised.
Enabling SELinux in container runtime provides an additional security control to help further enforce isolation among deployed containers and the host.
This guide describes how to enable SELinux in Kubernetes environment provided by k0s on CentOS and Red Hat Enterprise Linux (RHEL).
Requirements
- SELinux is enabled on host OS of the worker nodes.
- SELinux has the container-selinux policy installed.
- SELinux labels are correctly set for k0s installation files of the worker nodes.
- SELinux is enabled in container runtime such as containerd on the worker nodes.
Check whether SELinux is enabled
SELinux is enabled on CentOS and RHEL by default. Below command output indicates SELinux is enabled.
$ getenforce
EnforcingInstall container-selinux
It is required to have container-selinux installed.
In most Fedora based distributions including Fedora 37, Red Hat Enterprise Linux 7, 8 and 8, CentOS
7 and 8 and Rocky Linux 9 this can be achieved by installing the package container-selinux.
In RHEL 7 and CentOS 7 this is achieved by running:
yum install -y container-selinuxIn the rest of the metnioned distributions run:
dnf install -y container-selinuxSet SELinux labels for k0s installation files
Run below commands on the host OS of the worker nodes.
DATA_DIR="/var/lib/k0s"
sudo semanage fcontext -a -t container_runtime_exec_t "${DATA_DIR}/bin/containerd.*"
sudo semanage fcontext -a -t container_runtime_exec_t "${DATA_DIR}/bin/runc"
sudo restorecon -R -v ${DATA_DIR}/bin
sudo semanage fcontext -a -t container_var_lib_t "${DATA_DIR}/containerd(/.*)?"
sudo semanage fcontext -a -t container_ro_file_t "${DATA_DIR}/containerd/io.containerd.snapshotter.*/snapshots(/.*)?"
sudo restorecon -R -v ${DATA_DIR}/containerd
sudo semanage fcontext -a -t container_file_t "/etc/cni/.*"
sudo restorecon -R -v /etc/cni
sudo semanage fcontext -a -t container_file_t "/opt/cni/.*"
sudo restorecon -R -v /opt/cniEnable SELinux in containerd of k0s
Add below lines to /etc/k0s/containerd.d/selinux.toml of the worker nodes. k0s will automatically restart containerd with the new configuration if needed.
[plugins.'io.containerd.cri.v1.runtime']
enable_selinux = trueVerify SELinux works in Kubernetes environment
By following the example Assign SELinux labels to a Container, deploy a testing pod using below YAML file:
apiVersion: v1
kind: Pod
metadata:
name: test-selinux
spec:
containers:
- image: busybox
name: test-selinux
command: ["sleep", "infinity"]
securityContext:
seLinuxOptions:
level: "s0:c123,c456"After the pod starts, ssh to the worker node on which the pod is running and check the pod process. It should display the label s0:c123,c456 that you specified in YAML file:
$ ps -efZ | grep -F 'sleep infinity'
system_u:system_r:container_t:s0:c123,c456 root 3346 3288 0 16:39 ? 00:00:00 sleep infinityNode-Local Load Balancing (NLLB) SELinux Support
When using k0s's Node-Local Load Balancing (NLLB) feature with SELinux enabled, k0s automatically sets the correct SELinux labels on NLLB configuration files. The Envoy proxy configuration files in /run/k0s/nllb/envoy/ are labeled with container_file_t context, allowing the Envoy container to read them.
No manual configuration is required for NLLB to work with SELinux. The SELinux labeling is handled automatically when:
- The NLLB feature is enabled in the worker configuration
- SELinux is enabled on the system
- Containerd is configured with SELinux support (
enable_selinux = true)
You can verify the correct labels are set:
$ ls -lZ /run/k0s/nllb/envoy/
-r--r--r--. 1 root root system_u:object_r:container_file_t:s0 1122 Jan 19 22:26 cds.yaml
-r--r--r--. 1 root root system_u:object_r:container_file_t:s0 850 Jan 19 22:26 envoy.yamlShell Completion (`docs/shell-completion.md`)
Enabling Shell Completion
Introduction
Shell completion enhances the user experience by providing auto-completion for
commands in the terminal. K0s supports shell completion for the following
shells:
bash, GNU Bashzsh, the Z-shellfish, the friendly interactive shellpowershell, Microsoft PowerShell
General Usage
To generate a completion script for your shell, use the following command: k0s completion <shell_name>. Sourcing the completion script in your shell enables
k0s autocompletion.
bash
One-shot usage: source <(k0s completion bash).
This is a recipe to load completions for each new shell. Adjust to your personal
needs:
mkdir ~/.bash_completion.d
k0s completion bash >~/.bash_completion.d/k0s
cat <<'EOF' >~/.bashrc
for compFile in ~/.bash_completion.d/*; do
[ ! -f "$compFile" ] || source -- "$compFile"
done
unset compFile
EOFThen restart the shell or source ~/.bashrc.
zsh
One-shot usage: source <(k0s completion bash).
Following a recipe to load completions for each new shell. Adjust to your
personal needs. If shell completion is not already enabled in your zsh
environment you will need to enable it:
echo "autoload -Uz compinit; compinit" >>~/.zshrcPlace the completion script in a custom site-functions folder:
mkdir -p -- ~/.local/share/zsh/site-functions
k0s completion zsh >~/.local/share/zsh/site-functions/_k0sEdit ~/.zshrc and add the line fpath+=(~/.local/share/zsh/site-functions)
somewhere before compinit is called. After that, restart the shell.
When using Oh My ZSH!, you can create a custom plugin:
mkdir -- "$ZSH_CUSTOM/plugins/k0s"
cat <<'EOF' >"$ZSH_CUSTOM/plugins/k0s/k0s.plugin.zsh"
k0s completion zsh >| "$ZSH_CACHE_DIR/completions/_k0s" &|
EOF
omz plugin enable k0sThen restart the shell.
fish
One-shot usage: k0s completion fish | source.
This is a recipe to load completions for each new shell. Adjust to your personal
needs:
mkdir -p -- "${XDG_CONFIG_HOME:-$HOME/.config}/fish/completions"
k0s completion fish >"${XDG_CONFIG_HOME:-$HOME/.config}/fish/completions/k0s.fish"Then restart the shell.
powershell
Save the completion script into a file:
k0s completion powershell > C:\path\to\k0s.ps1You can import it like so:
Import-Module C:\path\to\k0s.ps1To automatically load the module for each new shell session, add the above line
to your shell profile. You can find the path to your profile via Write-Output $profile.
Single Stack Ipv6 (`docs/single-stack-ipv6.md`)
IPv6 single-stack networking
K0s introduced experimental IPv6 single-stack networking, allowing clusters to operate
exclusively with IPv6 addresses. IPv6 single stack is currently in alpha state and requires
the IPv6SingleStack feature gate to be enabled on every controller.
IPv6 single-stack is fully compatible with both node-local load balancing and
control plane load balancing.
Enabling IPv6 single-stack using the default CNI (Kube-router)
In order to enable IPv6 single-stack networking using the default CNI provider, both an IPv6podCIDR and serviceCIDR must be provided. Dual stack must not be enabled. Migrations
to dual stack are possible as long as the primary address family remains IPv6.
spec:
network:
podCIDR: fd00::/108
serviceCIDR: fd01::/108Ensure that every controller in the cluster is executed with the
IPv6SingleStack feature gate enabled:
k0s controller --feature-gates="IPv6SingleStack=true"This configuration will set up all Kubernetes components and Kube-router for IPv6
single-stack networking.
Configuring the node CIDR mask size
By default, k0s uses a /117 node CIDR mask size for IPv6, which provides
2048 IP addresses per node and and a /24 for IPv4 which provides 256
addresses per node.
Using the example configuration IPv6PodCIDR: fd00::/108, there are 9 bits
available for node allocation (117 - 108 = 9) and 11 bits available for
pod allocation (128 - 117 = 11). This allows 512 nodes with 2048 IPs per node.
You can customize the node CIDR mask size using the controller manager's extra arguments:
spec:
controllerManager:
extraArgs:
node-cidr-mask-size: "120"
network:
podCIDR: fd00::/108
serviceCIDR: fd01::/108With a /120 node CIDR mask size, the cluster would support more nodes (4096 nodes maximum
with the /108 pod CIDR) but each node would receive 256 IP addresses.
Important Notes for Kube-router
Currently kube-router on IPv6 doesn't pass the kubernetes network conformance tests.
Storage (`docs/storage.md`)
Storage
CSI
k0s supports a wide range of different storage options by utilizing Container Storage Interface (CSI). All Kubernetes storage solutions are supported and users can easily select the storage that fits best for their needs.
When the storage solution implements CSI, Kubernetes can communicate with the storage to create and configure persistent volumes. This makes it easy to dynamically provision the requested volumes. It also expands the supported storage solutions from the previous generation, in-tree volume plugins. More information about the CSI concept is described on the Kubernetes Blog.
Installing 3rd party storage solutions
Follow your storage driver's installation instructions. Note that by default the Kubelet installed by k0s uses a slightly different path for its working directory (/var/lib/k0s/kubelet instead of /var/lib/kubelet). Consult the CSI driver's configuration documentation on how to customize this path. The actual path can differ if you defined the flag --data-dir.
Example storage solutions
Different Kubernetes storage solutions are explained in the official Kubernetes storage documentation. All of them can be used with k0s. Here are some popular ones:
- Rook-Ceph (Open Source)
- GlusterFS (Open Source)
- Longhorn (Open Source)
- Amazon EBS
- Google Persistent Disk
- Azure Disk
- Portworx
If you are looking for a fault-tolerant storage with data replication, you can follow our tutorial for configuring Ceph storage with Rook.
Support Bundle Controller.Yaml (`docs/support-bundle-controller.yaml`)
apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: controller
spec:
uri: # TODO
collectors:
- clusterInfo: {}
- clusterResources:
namespaces:
- kube-system
- k0s-autopilot
- kube-node-lease
- default # so we get kubernetes svc endpoints
- nodeMetrics: {}
hostCollectors:
# System Info Collectors
- cpu: {}
- hostOS: {}
- hostServices: {}
- ipv4Interfaces: {}
- memory: {}
- time: {}
# Certificate Info for ETCD and K8s API
- certificate:
collectorName: k8s-api-keypair
certificatePath: /var/lib/k0s/pki/server.crt
keyPath: /var/lib/k0s/pki/server.key
- certificate:
collectorName: etcd-keypair
certificatePath: /var/lib/k0s/pki/etcd/server.crt
keyPath: /var/lib/k0s/pki/etcd/server.key
# Disk usage for commonly used directories in kURL installs
- diskUsage:
collectorName: root
path: /
# Run collectors for system information
- run:
collectorName: k8s-api-healthz-6443
command: "curl"
args: ["-k", "--cert", "/var/lib/k0s/pki/admin.crt", "--key", "/var/lib/k0s/pki/admin.key", "https://localhost:6443/healthz?verbose"]
- run:
collectorName: curl-etcd-health-2379
command: "curl"
args: ["-ki", "https://localhost:2379/health", "--cert", "/var/lib/k0s/pki/apiserver-etcd-client.crt", "--key", "/var/lib/k0s/pki/apiserver-etcd-client.key"]
- run:
collectorName: etcd-members
command: "k0s"
args: ["etcd", "member-list"]
- run:
collectorName: "free"
command: "free"
args: ["-m"]
- run:
collectorName: "top"
command: "top"
args: ["-b", "-n", "1"]
- run:
collectorName: "uptime"
command: "uptime"
args: []
- run:
collectorName: "uname"
command: "uname"
args: ["-a"]
- run:
collectorName: "df"
command: "df"
args: ["-h"]
- run:
collectorName: "iostat"
command: "iostat"
args: ["-x"]
# Systemctl service statuses
- run:
collectorName: "systemctl-firewalld-status"
command: "systemctl"
args: ["status", "firewalld"]
- run:
collectorName: "systemctl-ufw-status"
command: "systemctl"
args: ["status", "ufw"]
- run:
collectorName: "systemctl-k0s-status"
command: "systemctl"
args: ["status", "k0s*"]
# systemd Service Configurations
- run:
collectorName: "systemctl-cat-journald"
command: "systemctl"
args: ["cat", "systemd-journald"]
- run:
collectorName: "systemctl-cat-k0s"
command: "systemctl"
args: ["cat", "k0s*"]
# TODO Add same checks for rc-service
# Logs for k0s
- run:
collectorName: "journalctl-k0s"
command: "journalctl"
args: ["-u", "k0s*", "--no-pager", "-S", "7 days ago"]
- run:
collectorName: "journalctl-dmesg"
command: "journalctl"
args: ["--dmesg", "--no-pager", "-S", "7 days ago"]
# k0s status
- run:
collectorName: k0s-status
command: "k0s"
args: ["status", "-o", "yaml"]
# Gathering hostname info to help troubleshoot scenarios where the hostname mismatch
- run:
collectorName: "hostnames"
command: "sh"
args:
- -c
- |
echo "hostname = $(hostname)"
echo "/proc/sys/kernel/hostname = $(cat /proc/sys/kernel/hostname)"
echo "uname -n = $(uname -n)"
# System Info Collectors
- run:
collectorName: "vmstat"
command: "vmstat"
args: ["-w"]
- run:
collectorName: "ps-high-load"
command: "sh"
args: ["-c", "ps -eo s,user,cmd | grep ^[RD] | sort | uniq -c | sort -nbr | head -20"]
- filesystemPerformance:
collectorName: filesystem-latency-two-minute-benchmark
timeout: 2m
directory: /var/lib/k0s/etcd
fileSize: 22Mi
operationSizeBytes: 2300
datasync: true
enableBackgroundIOPS: true
backgroundIOPSWarmupSeconds: 10
backgroundWriteIOPS: 300
backgroundWriteIOPSJobs: 6
backgroundReadIOPS: 50
backgroundReadIOPSJobs: 1
exclude: true
- run:
collectorName: "localhost-ips"
command: "sh"
args: ["-c", "host localhost"]
hostAnalyzers:
- time:
checkName: "ntp-status"
outcomes:
- fail:
when: "ntp == unsynchronized+inactive"
message: "System clock is not synchronized"
- warn:
when: "ntp == unsynchronized+active"
message: System clock not yet synchronized
- pass:
when: "ntp == synchronized+active"
message: "System clock is synchronized"
- warn:
when: "timezone != UTC"
message: "Non UTC timezone can interfere with system function"
- pass:
when: "timezone == UTC"
message: "Timezone is set to UTC"
- diskUsage:
checkName: "root"
collectorName: "root"
outcomes:
- fail:
when: "total < 40Gi"
message: The disk containing directory / has less than 40Gi of total space
- warn:
when: "used/total > 80%"
message: The disk containing directory / is more than 80% full
- warn:
when: "available < 10Gi"
message: The disk containing directory / has less than 10Gi of disk space available
- pass:
message: The disk containing directory / has sufficient space
- diskUsage:
checkName: "tmp"
collectorName: "tmp"
outcomes:
- warn:
when: "total < 8Gi"
message: The disk containing directory /tmp has less than 8Gi of total space
- warn:
when: "used/total > 80%"
message: The disk containing directory /tmp is more than 80% full
- warn:
when: "available < 2Gi"
message: The disk containing directory /tmp has less than 2Gi of disk space available
- pass:
message: The disk containing directory /tmp has sufficient space
- filesystemPerformance:
collectorName: filesystem-latency-two-minute-benchmark
outcomes:
- pass:
when: "p99 < 10ms"
message: "Write latency is ok (p99 target < 10ms)"
- warn:
message: "Write latency is high. p99 target >= 10ms)"
exclude: true
analyzers:
- textAnalyze:
checkName: Kubernetes API health check
fileName: host-collectors/run-host/k8s-api-healthz-6443.txt
regex: ".healthz check passed"
outcomes:
- fail:
when: "false"
message: "Kubernetes API health check did not pass. One or more components are not working."
- pass:
when: "true"
message: "Kubernetes API health check passed"
- textAnalyze:
checkName: ETCD API Health
fileName: host-collectors/run-host/curl-etcd-health-2379.txt
regex: "."health":"true""
outcomes:
- fail:
when: "false"
message: "ETCD status returned: unhealthy"
- pass:
when: "true"
message: "ETCD status returned: healthy"
- textAnalyze:
checkName: Check if localhost resolves to 127.0.0.1
fileName: host-collectors/run-host/localhost-ips.txt
regex: 'localhost has address 127.0.0.1'
outcomes:
- fail:
when: "false"
message: "'localhost' does not resolve to 127.0.0.1 ip address"
- pass:
when: "true"
message: "'localhost' resolves to 127.0.0.1 ip address"
Support Bundle Worker.Yaml (`docs/support-bundle-worker.yaml`)
Spec to run when a kURL cluster is down and in-cluster specs can't be run
apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: worker
spec:
uri: #TODO
hostCollectors:
# System Info Collectors
- blockDevices: {}
- cpu: {}
- hostOS: {}
- hostServices: {}
- ipv4Interfaces: {}
- memory: {}
- time: {}
- ipv4Interfaces: {}
# Disk usage for commonly used directories in kURL installs
- diskUsage:
collectorName: root
path: /
- diskUsage:
collectorName: tmp
path: /tmp
- diskUsage:
collectorName: var-lib-k0s
path: /var/lib/k0s
# Run collectors for system information
- run:
collectorName: k8s-api-healthz-6443
command: "k0s"
args: ["kubectl", "--kubeconfig", "/var/lib/k0s/kubelet.conf", "get", "--raw", "/healthz?verbose"]
- run:
collectorName: "free"
command: "free"
args: ["-m"]
- run:
collectorName: "top"
command: "top"
args: ["-b", "-n", "1"]
- run:
collectorName: "uptime"
command: "uptime"
args: []
- run:
collectorName: "uname"
command: "uname"
args: ["-a"]
- run:
collectorName: "df"
command: "df"
args: ["-h"]
- run:
collectorName: "iostat"
command: "iostat"
args: ["-x"]
# SELinux status
- run:
collectorName: "sestatus"
command: "sestatus"
args: []
- run:
collectorName: "apparmor-status"
command: "apparmor_status"
args: []
- run:
collectorName: "crictl-ps"
command: "k0s"
args: ["ctr", "c", "ls"]
- run:
collectorName: "iptables"
command: "/var/lib/k0s/bin/iptables"
args: ["-L", "-v"]
- run:
collectorName: "iptables-version"
command: "/var/lib/k0s/bin/iptables"
args: ["-V"]
- run:
collectorName: "lsblk"
command: "lsblk"
args: ["--fs"]
- run:
collectorName: "netstat-ports"
command: "netstat"
args: ["-t", "-u", "-l", "-p", "-n"]
- run:
collectorName: "netstat-route-table"
command: "netstat"
args: ["-r", "-n"]
- run:
collectorName: "ip-route-table"
command: "ip"
args: ["route"]
- run:
collectorName: "sysctl"
command: "sysctl"
args: ["-a"]
# Systemctl service statuses for CRI, Kubelet, and Firewall
- run:
collectorName: "systemctl-k0s-status"
command: "systemctl"
args: ["status", "k0s*"]
# systemd Service Configurations for CRI, Kubelet
- run:
collectorName: "systemctl-cat-journald"
command: "systemctl"
args: ["cat", "systemd-journald"]
- run:
collectorName: "systemctl-cat-k0s"
command: "systemctl"
args: ["cat", "k0s*"]
# Logs for CRI, Kubelet, Kernel
- run:
collectorName: "journalctl-k0s"
command: "journalctl"
args: ["-u", "k0s*", "--no-pager", "-S", "7 days ago"]
- run:
collectorName: "journalctl-dmesg"
command: "journalctl"
args: ["--dmesg", "--no-pager", "-S", "7 days ago"]
# sysctl parameters
- run:
collectorName: "sysctl-all"
command: "sh"
args: ["-c", "sysctl --all 2>/dev/null"]
- run:
collectorName: "k0s-sysinfo"
command: "k0s"
args: ["sysinfo"]
# Gathering hostname info to help troubleshoot scenarios where the hostname mismatch
- run:
collectorName: "hostnames"
command: "sh"
args:
- -c
- |
echo "hostname = $(hostname)"
echo "/proc/sys/kernel/hostname = $(cat /proc/sys/kernel/hostname)"
echo "uname -n = $(uname -n)"
# System Info Collectors
- run:
collectorName: "du-root"
command: "sh"
args: ["-c", "du -Shax / --exclude /proc | sort -rh | head -20"]
- run:
collectorName: "mount"
command: "mount"
args: ["-l"]
- run:
collectorName: "vmstat"
command: "vmstat"
args: ["-w"]
- run:
collectorName: "ps-high-load"
command: "sh"
args: ["-c", "ps -eo s,user,cmd | grep ^[RD] | sort | uniq -c | sort -nbr | head -20"]
- run:
collectorName: "ps-detect-antivirus-and-security-tools"
command: "sh"
args: [-c, "ps -ef | grep -E 'clamav|sophos|esets_daemon|fsav|symantec|mfend|ds_agent|kav|bdagent|s1agent|falcon|illumio' | grep -v grep"]
- run:
collectorName: "localhost-ips"
command: "sh"
args: ["-c", "host localhost"] hostAnalyzers:
- time:
checkName: "ntp-status"
outcomes:
- fail:
when: "ntp == unsynchronized+inactive"
message: "System clock is not synchronized"
- warn:
when: "ntp == unsynchronized+active"
message: System clock not yet synchronized
- pass:
when: "ntp == synchronized+active"
message: "System clock is synchronized"
- warn:
when: "timezone != UTC"
message: "Non UTC timezone can interfere with system function"
- pass:
when: "timezone == UTC"
message: "Timezone is set to UTC"
- diskUsage:
checkName: "root"
collectorName: "root"
outcomes:
- fail:
when: "total < 40Gi"
message: The disk containing directory / has less than 40Gi of total space
- warn:
when: "used/total > 80%"
message: The disk containing directory / is more than 80% full
- warn:
when: "available < 10Gi"
message: The disk containing directory / has less than 10Gi of disk space available
- pass:
message: The disk containing directory / has sufficient space
- diskUsage:
checkName: "tmp"
collectorName: "tmp"
outcomes:
- warn:
when: "total < 8Gi"
message: The disk containing directory /tmp has less than 8Gi of total space
- warn:
when: "used/total > 80%"
message: The disk containing directory /tmp is more than 80% full
- warn:
when: "available < 2Gi"
message: The disk containing directory /tmp has less than 2Gi of disk space available
- pass:
message: The disk containing directory /tmp has sufficient space
- diskUsage:
checkName: "var-lib-k0s"
collectorName: "var-lib-k0s"
outcomes:
- warn:
when: "used/total > 80%"
message: The disk containing directory /var/lib/k0s is more than 80% full
- warn:
when: "available < 10Gi"
message: The disk containing directory /var/lib/k0s has less than 10Gi of disk space available
- pass:
message: The disk containing directory /var/lib/k0s has sufficient space
- filesystemPerformance:
collectorName: filesystem-latency-two-minute-benchmark
outcomes:
- pass:
when: "p99 < 10ms"
message: "Write latency is ok (p99 target < 10ms)"
- warn:
message: "Write latency is high. p99 target >= 10ms)"
exclude: true analyzers:
- textAnalyze:
checkName: Hostname Mismatch
fileName: host-collectors/run-host/journalctl-k0s.txt
regex: ".can only access node lease with the same name as the requesting node."
outcomes:
- fail:
when: "true"
message: "Possible hostname change. Verify that the current hostname matches what's expected by the k8s control plane"
- pass:
when: "false"
message: "No signs of hostname changes found"
- textAnalyze:
checkName: Kubernetes API health check
fileName: host-collectors/run-host/k8s-api-healthz-6443.txt
regex: ".healthz check passed"
outcomes:
- fail:
when: "false"
message: "Kubernetes API health check did not pass. One or more components are not working."
- pass:
when: "true"
message: "Kubernetes API health check passed"
- textAnalyze:
checkName: Check if localhost resolves to 127.0.0.1
fileName: host-collectors/run-host/localhost-ips.txt
regex: 'localhost has address 127.0.0.1'
outcomes:
- fail:
when: "false"
message: "'localhost' does not resolve to 127.0.0.1 ip address"
- pass:
when: "true"
message: "'localhost' resolves to 127.0.0.1 ip address"
- textAnalyze:
checkName: Check if SELinux is enabled
fileName: host-collectors/run-host/sestatus.txt
regex: '(?m)^Current mode:\s+enforcing'
ignoreIfNoFiles: true
outcomes:
- fail:
when: "true"
message: "SELinux is enabled when it should be disabled for kubernetes to work properly"
- pass:
when: "false"
message: "SELinux is disabled as expected"
- textAnalyze:
checkName: "Detect Threat Management and Network Security Tools"
fileName: host-collectors/run-host/ps-detect-antivirus-and-security-tools.txt
regex: '\b(clamav|sophos|esets_daemon|fsav|symantec|mfend|ds_agent|kav|bdagent|s1agent|falcon|illumio)\b'
ignoreIfNoFiles: true
outcomes:
- fail:
when: "true"
message: "Antivirus or Network Security tools detected. These tools can interfere with kubernetes operation."
- pass:
when: "false"
message: "No Antivirus or Network Security tools detected."
System Monitoring (`docs/system-monitoring.md`)
System components monitoring
Controller nodes are isolated by default, which thus means that a cluster user cannot schedule workloads onto controller nodes.
k0s provides a mechanism to expose system components for monitoring. System component metrics can give a better look into what is happening inside them. Metrics are particularly useful for building dashboards and alerts.
You can read more about metrics for Kubernetes system components in the Kubernetes documentation.
Note: the mechanism is an opt-in feature, you can enable it on installation:
sudo k0s install controller --enable-metrics-scraperOnce enabled, a new set of objects will appear in the cluster:
❯ ~ kubectl get all -n k0s-system
NAME READY STATUS RESTARTS AGE
pod/k0s-pushgateway-6c5d8c54cf-bh8sb 1/1 Running 0 43h
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/k0s-pushgateway ClusterIP 10.100.11.116 <none> 9091/TCP 43h
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/k0s-pushgateway 1/1 1 1 43h
NAME DESIRED CURRENT READY AGE
replicaset.apps/k0s-pushgateway-6c5d8c54cf 1 1 1 43hThat's not enough to start scraping these additional metrics. For Prometheus
Operator based solutions, you can create aServiceMonitor for it like this:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: k0s
namespace: k0s-system
spec:
endpoints:
- port: http
honorLabels: true
selector:
matchLabels:
app: k0s-observability
component: pushgateway
k0s.k0sproject.io/stack: metricsNote that it won't clear alerts like KubeControllerManagerDown orKubeSchedulerDown as they are based on Prometheus' internal up metrics. But
you can get rid of these alerts by modifying them to detect a working component
like this:
absent(apiserver_audit_event_total{job="kube-scheduler"})Jobs
The list of components which is scrapped by k0s:
- kube-scheduler
- kube-controller-manager
- etcd
- kine
Note: kube-apiserver metrics are not scrapped since they are accessible via kubernetes endpoint within the cluster.
Architecture
k0s uses a pushgateway with a TTL to make it possible to detect issues with the metrics delivery. The default TTL is 2 minutes.
System Requirements (`docs/system-requirements.md`)
System requirements
This page describes the system requirements for k0s.
Minimum memory and CPU requirements
The minimum requirements for k0s detailed below are approximations, and thus your results may vary.
| Role | Memory (RAM in GB) | Virtual CPUs (vCPU) |
|---|---|---|
| Controller node | 1 | 1 |
| Worker node | 0.5 | 1 |
| Controller + worker | 1 | 1 |
Controller node recommendations
| # of worker nodes | # of pods | Recommended RAM (in GB) | Recommended vCPU |
|---|---|---|---|
| 10 | 1000 | 1-2 | 1-2 |
| 50 | 5000 | 2-4 | 2-4 |
| 100 | 10000 | 4-8 | 2-4 |
| 500 | 50000 | 8-16 | 4-8 |
| 1000 | 100000 | 16-32 | 8-16 |
| 5000 | 150000 | 32-64 | 16-32 |
k0s has the standard Kubernetes limits for the maximum number of nodes, pods, etc. For more details, see the Kubernetes considerations for large clusters.
k0s controller node measured memory consumption can be found below on this page.
Storage
It's recommended to use an SSD for optimal storage performance (cluster latency and throughput are sensitive to storage).
For worker nodes it is required that there is at least 15% relative disk space free.
The specific storage consumption for k0s is as follows:
| Role | Usage (k0s part) | Minimum required |
|---|---|---|
| Controller node | ~0.5 GB | ~0.5 GB |
| Worker node | ~1.3 GB | ~1.6 GB |
| Controller + worker | ~1.7 GB | ~2.0 GB |
Note: The operating system and application requirements must be considered in addition to the k0s part.
Host operating system
k0s runs on Linux and Windows operating systems.
The following operating systems are automatically tested as part of our CI:
| Operating System | Version/Notes |
|---|---|
| Amazon Linux | 2023 |
| Alpine Linux | 3.21, 3.23 |
| CentOS Stream | 9, 10 (Coughlan) |
| Debian GNU/Linux | 11 (bullseye) (supported until 2026-08-31), 12 (bookworm) (supported until 2028-06-30) |
| Fedora CoreOS | stable stream |
| Fedora Linux | 41 (Cloud Edition) |
| Flatcar Container Linux | by Kinvolk |
| Red Hat Enterprise Linux | 7.9 (Maipo), 8.10 (Ootpa), 9.7 (Plow) |
| Rocky Linux | 8.10 (Green Obsidian), 9.5 (Blue Onyx) |
| SUSE Linux Enterprise Server | 15 SP6 |
| Ubuntu | 20.04 LTS, 22.04 LTS, 24.04 |
Note: For detailed Linux-specific requirements, please refer to the Linux specific requirements.
Architecture
x86_64aarch64armv7lriscv64(No pre-compiled binaries, no CI coverage)
Networking
For information on the required ports and protocols, refer to networking.
External runtime dependencies
k0s strives to be as independent from the operating system as possible. See the
dedicated section on external runtime dependencies
for details on current and past requirements.
To run some automated compatiblility checks on your system, usek0s sysinfo.
Controller node measured memory consumption
The following table shows the measured memory consumption in the cluster of one controller node.
| # of Worker nodes | # of Pods (besides default) | Memory consumption (in MB) |
|---|---|---|
| 1 | 0 | 510 |
| 1 | 100 | 600 |
| 20 | 0 | 660 |
| 20 | 2000 | 1000 |
| 50 | 0 | 790 |
| 50 | 5000 | 1400 |
| 100 | 0 | 1000 |
| 100 | 10000 | 2300 |
| 200 | 0 | 1500 |
| 200 | 20000 | 3300 |
Measurement details:
- k0s v1.22.4+k0s.2 (default configuration with etcd)
- Ubuntu Server 20.04.3 LTS, OS part of the used memory was around 180 MB
- Hardware: AWS t3.xlarge (4 vCPUs, 16 GB RAM)
- Pod image: nginx:1.21.4
Upgrade (`docs/upgrade.md`)
Upgrade
The k0s upgrade is a simple process due to its single binary distribution. The k0s single binary file includes all the necessary parts for the upgrade and essentially the upgrade process is to replace that file and restart the service.
This tutorial explains two different approaches for k0s upgrade:
Upgrade a k0s node locally
If your k0s cluster has been deployed with k0sctl, then k0sctl provides the easiest upgrade method. In that case jump to the next chapter. However, if you have deployed k0s without k0sctl, then follow the upgrade method explained in this chapter.
Before starting the upgrade, consider moving your applications to another node if you want to avoid downtime. This can be done by draining a worker node. Remember to uncordon the worker node afterwards to tell Kubernetes that it can resume scheduling new pods onto the node.
The upgrade process is started by stopping the currently running k0s service.
sudo k0s stopNow you can replace the old k0s binary file. The easiest way is to use the download script. It will download the latest k0s binary and replace the old binary with it. You can also do this manually without the download script.
curl --proto '=https' --tlsv1.2 -sSf https://get.k0s.sh | sudo shThen you can start the service (with the upgraded k0s) and your upgrade is done.
sudo k0s startUpgrade a k0s cluster using k0sctl
The upgrading of k0s clusters using k0sctl occurs not through a particular
command (there is no upgrade sub-command in k0sctl) but by way of the
configuration file. The configuration file describes the desired state of the
cluster. When you pass the description to the k0sctl apply command, the system
discovers the current state and performs whatever actions are necessary to bring
the cluster to the desired state. For example, the system might perform an
upgrade.
k0sctl cluster upgrade process
The following operations occur during a k0sctl upgrade:
Upgrade of each controller, one at a time. There is no downtime if multiple controllers are configured.
Upgrade of workers, in batches of 10%.
Draining of workers, which allows the workload to move to other nodes prior to the actual upgrade of the worker node components. (To skip the drain process, use the
--no-drainoption.)The upgrade process continues once the upgraded nodes return to Ready state.
You can configure the desired cluster version in the k0sctl configuration by setting the value of spec.k0s.version:
spec:
k0s:
version: {{{ k0s_version }}}If you do not specify a version, k0sctl checks online for the latest version and defaults to it.
k0sctl apply...
...
INFO[0001] ==> Running phase: Upgrade controllers
INFO[0001] [ssh] 10.0.0.23:22: starting upgrade
INFO[0001] [ssh] 10.0.0.23:22: Running with legacy service name, migrating...
INFO[0011] [ssh] 10.0.0.23:22: waiting for the k0s service to start
INFO[0016] ==> Running phase: Upgrade workers
INFO[0016] Upgrading 1 workers in parallel
INFO[0016] [ssh] 10.0.0.17:22: upgrade starting
INFO[0027] [ssh] 10.0.0.17:22: waiting for node to become ready again
INFO[0027] [ssh] 10.0.0.17:22: upgrade successful
INFO[0027] ==> Running phase: Disconnect from hosts
INFO[0027] ==> Finished in 27s
INFO[0027] k0s cluster version {{{ k0s_version }}} is now installed
INFO[0027] Tip: To access the cluster you can now fetch the admin kubeconfig using:
INFO[0027] k0sctl kubeconfigUser Management (`docs/user-management.md`)
User Management
Kubernetes, and thus k0s, does not have any built-in functionality to manage users. Kubernetes relies solely on external sources for user identification and authentication. A client certificate is considered an external source in this case as Kubernetes API server "just" validates that the certificate is signed by a trusted CA. This means that it is recommended to use e.g. OpenID Connect to configure the API server to trust tokens issued by an external Identity Provider.
k0s comes with some helper commands to create kubeconfig with client certificates for users. There are few caveats that one needs to take into consideration when using client certificates:
- Client certificates have long expiration time, they're valid for one year
- Client certificates cannot be revoked (general Kubernetes challenge)
Adding a Cluster User
Run the kubeconfig create command on the controller to add a user to the cluster. The command outputs a kubeconfig for the user, to use for authentication.
k0s kubeconfig create [username]Enabling Access to Cluster Resources
Create the user with the system:masters group to grant the user access to the cluster:
k0s kubeconfig create --groups "system:masters" testUser > k0s.configCreate a roleBinding to grant the user access to the resources:
k0s kubectl create clusterrolebinding --kubeconfig k0s.config testUser-admin-binding --clusterrole=admin --user=testUserVerifying Signs (`docs/verifying-signs.md`)
Verifying Signed Binaries
K0smotron team provides signed binaries for k0s. The signatures are created using cosign.
Public key and signature files are available for download from the releases page.
Binaries can be verified using the cosign tool, for example:
{% set k0s_url_ver = k0s_version | urlencode -%}
cosign verify-blob \
--key https://github.com/k0sproject/k0s/releases/download/{{{ k0s_url_ver }}}/cosign.pub \
--signature https://github.com/k0sproject/k0s/releases/download/{{{ k0s_url_ver }}}/k0s-{{{ k0s_url_ver }}}-amd64.sig \
k0s-{{{ k0s_version }}}-amd64Version Skew Policy (`docs/version-skew-policy.md`)
Version skew policy
k0s ships a curated set of Kubernetes components (kube-apiserver,
kube-controller-manager, kube-scheduler, kubelet, kube-proxy, …) for each
release, so the version skew between those components inside a single k0s
release is always supported by upstream Kubernetes by construction.
What you still control as an operator is the skew between releases of
k0s that you mix in one cluster — for example, controllers running k0sv1.31.x+k0s.0 while workers still run v1.30.x+k0s.0 during a rolling
upgrade.
Supported skew
k0s is designed and tested for clusters in which all nodes run either the same
minor version or two adjacent ones, as happens during a rolling upgrade. In
practice, that means:
- Controllers must all be within one minor of each other.
- Workers may be one minor older than the controllers, but must never
be newer. This only applies to cluster upgrades. When bootstrapping a new
cluster, workers must run the same minor as the controllers.
Patch-version differences inside the same minor are always supported and are the
expected state during a phased upgrade.
Note that the upstream Kubernetes version skew policy permits wider skews,
such as kubelets up to three minors older than the API server. k0s doesn't
support those. Each k0s version is only expected to interoperate with the
previous one, and upgrades proceed one minor at a time.
Upgrades
When upgrading k0s in place:
- Upgrade controllers first, one minor at a time (e.g. v1.30 → v1.31,
not v1.30 → v1.32). - Upgrade workers after the control plane is fully on the new minor.
- Consider moving to the latest patch release of the old minor first. This
isn't required, but it takes the old minor's latest fixes along into the
upgrade.
Autopilot, k0sctl, and k0smotron all upgrade controllers before workers.
When upgrading manually, keep to the same order.
Downgrades
Downgrades are not supported by upstream Kubernetes and are not tested
by k0s. If a release needs to be rolled back, restore from a
backup taken before the upgrade rather than running an older
k0s binary against a newer cluster state.
Major versions
There is currently no Kubernetes v2 and no k0s v2, so major-version skew is
not a configuration that exists in practice.
Worker Node Config (`docs/worker-node-config.md`)
Configuration options for worker nodes
Although the k0s worker command does not take in any special YAML configuration, there are still methods for configuring the workers to run various components.
Node labels
The k0s worker command accepts the --labels flag, with which you can make the newly joined worker node register itself, in the Kubernetes API, with the given set of labels.
For example, running the worker with k0s worker --token-file k0s.token --labels="k0sproject.io/foo=bar,k0sproject.io/other=xyz" results in:
{% set kubelet_ver = k8s_version + '+k0s' -%}
{% set kubelet_ver_len = kubelet_ver | length -%}
$ kubectl get node --show-labels
NAME STATUS ROLES AGE {{{ 'VERSION' | ljust(kubelet_ver_len) }}} LABELS
worker0 NotReady <none> 10s {{{ kubelet_ver | ljust(kubelet_ver_len) }}} beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,k0sproject.io/foo=bar,k0sproject.io/other=xyz,kubernetes.io/arch=amd64,kubernetes.io/hostname=worker0,kubernetes.io/os=linuxController worker nodes are assigned node.k0sproject.io/role=control-plane and node-role.kubernetes.io/control-plane=true labels:
$ kubectl get node --show-labels
NAME STATUS ROLES AGE {{{ 'VERSION' | ljust(kubelet_ver_len) }}} LABELS
controller0 NotReady control-plane 10s {{{ kubelet_ver | ljust(kubelet_ver_len) }}} beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/hostname=worker0,kubernetes.io/os=linux,node.k0sproject.io/role=control-plane,node-role.kubernetes.io/control-plane=trueNote: Setting the labels is only effective on the first registration of the node. Changing the labels thereafter has no effect.
Taints
The k0s worker command accepts the --taints flag, with which you can make the newly joined worker node register itself with the given set of taints.
Note: Controller nodes running with --enable-worker are assigned node-role.kubernetes.io/control-plane:NoExecute taint automatically. You can disable default taints using --no-taints parameter.
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taintsNAME TAINTS
controller0 [map[effect:NoSchedule key:node-role.kubernetes.io/control-plane]]
worker0 <none>Kubelet configuration
The k0s worker command accepts a generic flag to pass in any set of arguments
for the kubelet process.
For example, running k0s worker --token-file=k0s.token --kubelet-extra-args="--node-ip=1.2.3.4 --address=0.0.0.0" passes in the given
flags to Kubelet as-is. As such, you must confirm that any flags you are passing
in are properly formatted and valued as k0s will not validate those flags.
Worker Profiles
Kubelet configuration fields can also be set via worker profiles. Worker
profiles are defined in the main k0s.yaml and are used to generate ConfigMaps
containing a custom kubelet.config.k8s.io/v1beta1/KubeletConfiguration
object.
See also the examples of k0s.yaml containing worker
profiles and the list of possible
Kubelet configuration
fields.
Kubelet serving certificates
By default, k0s configures kubelets to request their serving certificates from
the Kubernetes API via Certificate Signing Requests (CSRs), by enablingserverTLSBootstrap in the generated kubelet configuration. The kubelet then
requests a certificate from the kubernetes.io/kubelet-serving signer and uses
it to serve its own API. Clients of that API verify the certificate against the
cluster CA. The Kubernetes API server verifies it, for example, when servingkubectl logs and kubectl exec requests. Similarly, k0s's metrics-server
component, which bundles the Kubernetes Metrics Server, verifies it when
scraping the kubelet's resource metrics. Other properly configured monitoring
systems do the same.
Kubernetes doesn't approve these CSRs automatically. k0s ships a controller
component called csr-approver that does so, provided the request meets all of
the following conditions:
- The PEM-encoded certificate request size doesn't exceed 512 KiB.
- It is a well-formed kubelet serving certificate request, according to
Kubernetes' validation rules. - The certificate request has no more than 4096 SANs, counting DNS names and IP
addresses together. - It was created by the very node it requests the certificate for. The
requesting user must have thesystem:node:<nodeName>name and be a member of
thesystem:nodesgroup, and the certificate's common name must be identical
to the requesting user name. - The requested DNS names and IP addresses don't identify the cluster's control
plane or other in-cluster services: no IP address may be inside the service
CIDR(s), and no DNS name may bekubernetes,kubernetes.default, or a name
within thesvcor cluster domains. - The node exists in the cluster.
- The node's
status.addressescontains no more than 4096 entries. - The requested DNS names and IP addresses are all listed in the node's
status.addresses.
Requests that don't meet these conditions are left pending, and the reason is
logged by the k0s controller. Note that a node's addresses are reported by its
kubelet, so flags like --node-ip directly influence which addresses a
certificate may be issued for.
The CSR approver checks for pending requests periodically. It only considers the
newest pending request of each node per pass, so that a node can't hold up
others by creating large numbers of requests. It can be turned off via k0s controller --disable-components csr-approver. In that case, kubelet serving
certificates need to be approved by some other means, otherwise the affected
kubelet APIs remain unavailable.
IPTables Mode
k0s detects the iptables backend automatically based on the existing records. On a brand-new setup, iptables-nft will be used.
There is an --iptables-mode flag to specify the mode explicitly. Valid values: nft, legacy and auto (default).
k0s worker --iptables-mode=nftArchitecture Adr 001 Autopilot Oci Basic Auth Support (`docs/architecture/adr-001-autopilot-oci-basic-auth-support.md`)
ADR 1: Add Support for OCI Registry and HTTP Authentication
Context
Registries are increasingly being used as generic artifact stores, expanding
beyond their traditional role of hosting container images. To align with this
trend, it is beneficial for Autopilot to support pulling artifacts directly
from registries. Currently, Autopilot's capabilities are limited to downloading
artifacts via the HTTP[S] protocols.
Enhancing Autopilot to pull artifacts directly from registries will streamline
workflows and improve efficiency by allowing integration and deployment of
diverse artifacts without relying solely on HTTP[S] endpoints. This update
will enable Autopilot to handle registry-specific protocols and authentication
mechanisms, aligning it with modern deployment practices.
Currently, Autopilot does not support the retrieval of artifacts via the HTTP
protocol when authentication is required. Implementing this feature to
accommodate such authentication methods would be a valuable enhancement.
Decision
Implement support in Autopilot for pulling artifacts, such as k0s binaries and
image bundles, directly from a registry using the
ORAS client. Additionally, add support for HTTP
authentication to ensure secure access to artifacts.
Solution
Starting with the current PlanResourceURL struct:
type PlanResourceURL struct {
// URL is the URL of a downloadable resource.
URL string `json:"url"`
// Sha256 provides an optional SHA256 hash of the URL's content for
// verification.
Sha256 string `json:"sha256,omitempty"`
}We must specify to Autopilot where to access credentials for remote artifact
pulls. This will be achieved by adjusting the struct as follows:
type PlanResourceURL struct {
// URL is the URL of a downloadable resource.
URL string `json:"url"`
// Sha256 provides an optional SHA256 hash of the URL's content for
// verification.
Sha256 string `json:"sha256,omitempty"`
// SecretRef holds a reference to a secret where the credentials are
// stored. We use these credentials when pulling the artifacts from the
// provided URL using
// any of the supported protocols (http, https, and oci).
SecretRef *corev1.SecretReference `json:"secretRef,omitempty"`
// InsecureSkipTLSVerify indicates whether certificates in the remote
// URL (if using TLS) can be ignored.
InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"`
}SecretRef property is of type SecretReference as defined byk8s.io/api/core/v1 package. The secret pointed by the provided SecretRef
will be used for pulling artifacts using either HTTP[S] or OCI protocols.
Example Configurations
Configuration for OCI
url: oci://my.registry/binaries/k0s:v1.30.1+k0s.0
sha256: e95603f167cce6e3cffef5594ef06785b3c1c00d3e27d8e4fc33824fe6c38a99
secretRef:
namespace: kube-system
name: artifacts-registryConfiguration for OCI using plain HTTP transport
url: oci+http://my.registry/binaries/k0s:v1.30.1+k0s.0
sha256: e95603f167cce6e3cffef5594ef06785b3c1c00d3e27d8e4fc33824fe6c38a99
secretRef:
namespace: kube-system
name: artifacts-registryConfiguration for HTTPS
url: https://my.file.server/binaries/k0s-v1.30.1+k0s.0
sha256: e95603f167cce6e3cffef5594ef06785b3c1c00d3e27d8e4fc33824fe6c38a99
secretRef:
namespace: kube-system
name: artifacts-basic-authConfiguration for HTTP
url: http://my.file.server/binaries/k0s-v1.30.1+k0s.0
sha256: e95603f167cce6e3cffef5594ef06785b3c1c00d3e27d8e4fc33824fe6c38a99
secretRef:
namespace: kube-system
name: artifacts-token-based-authSecrets Layout
The following standard Kubernetes secret types are supported:
kubernetes.io/basic-auth
The username and password are used according to the protocol's standard
procedure for password-based authentication.kubernetes.io/dockerconfigjson
It works in the same way as a Pod's image pull secret. Only supported for
theoci://protocol. (Might be supported for other protocols in the future,
as well).
Potentially supported in the future:
kubernetes.io/tls
For TLS client authentication.
Moreover, k0s supports the following custom secret type:
k0sproject.io/http-authorization-header
Sets a custom value for the HTTP Authorization header:yamlapiVersion: v1 kind: Secret metadata: name: creds namespace: kube-system data: authorization: "Bearer abc123def456ghi789jkl0"The
authorizationentry is used as is, with its content placed directly into
theAuthorizationheader. A secret like the above will make Autopilot set
theAuthorizationheader toBearer abc123def456ghi789jkl0.
Additional Details
- The
InsecureSkipTLSVerifyproperty is equivalent to definingInsecureSkipTLSVerifyon a Go HTTP client. - The
InsecureSkipTLSVerifyproperty will be valid for bothoci://andhttps://protocols. It has no effect for theoci+http://andhttp://
protocols. - If a protocol is not specified or an incorrect one is provided, an error
state should be activated. - If no
SecretRefis defined, access will be anonymous (no authentication).
Status
Proposed
Consequences
- Users will have an additional protocol to be aware of.
- If the Secret referenced by
SecretRefdoes not exist, the download will
fail. - Users need to be notified about different failure types (e.g., unreadable
secret, invalid secret). - Additional configuration is required to handle authentication, ensuring
secure access to resources. - We will allow downloads from remote places using self-signed certificates if
requested to.
Contributors Code Guidelines (`docs/contributors/code_guidelines.md`)
Code guidelines
This page collects guidelines for the k0s code base. Many of them stem from
things that come up frequently during code reviews, or from k0s design
constraints, such as being a per-node agent. These guidelines complement
linters, which enforce general formatting and Go coding style. However, linters
perform static code analysis and can't judge broader concepts, such as whether a
change is the right size or if a comment is appropriate and accurate.
None of this is set in stone. The right trade-off depends on what a particular
change does, and reviewers may weigh things differently from case to case. The
guidelines also evolve with the project: if one of them stops serving its
purpose, change it rather than working around it.
Existing code doesn't follow all of this. When touching such code, prefer these
guidelines over consistency with the surrounding code, but don't churn unrelated
lines just to bring them in line.
Size the code to the problem
Match the amount of structure to what the change needs. A few lines at the call
site don't need a helper, a helper doesn't need to be exported, and neither
needs its own abstraction until there's a second user. Extra structure isn't
free; it creates more surface area to read, review, and maintain.
Remove dead code when you come across it, but keep such cleanups separate from
functional changes, at least in the commit message.
Comments
Comments cost readers time every time they read the code. Keep them crisp. Don't
comment on obvious things or things that readers can quickly figure out. Comment
on what they can't, such as ordering requirements, timing assumptions, or
compatibility with earlier versions. Judge a comment based on what it adds, not
by its length. Restating what the code already says is unhelpful even in a
single line, whereas a comment that explains an important, broader concept which
can't be better documented elsewhere may span several sentences or even
paragraphs.
Comments describe the present, not the past. Version control holds the history.
A comment that explains what the code used to do, which bug motivated a test, or
how a change came about belongs in the commit message, not in the code.
Avoid referencing file names or line numbers in comments. They become outdated
with the next refactoring, and nothing will tell you. Refer to Go identifiers
instead, using doc links where possible, so that the reference stays navigable
and tooling catches renames.
Errors
An error message is a fragment that gets chained with others through wrapping.
Keep it lowercase and without trailing punctuation, as per the Go
conventions, so that failed to apply stack: cannot create resource: connection refused reads as one line.
Add context to an error only if it's neither part of the error already nor known
to the caller anyway. Many errors, such as those returned by the standard
library's I/O functions, are self-describing, and the caller knows which
function it called. Wrapping them with a message that restates the operation
adds noise, not information.
Logging
Logs are the primary tool users have to understand what k0s is doing on their
systems. They're often read out of context, in between lots of other output, by
someone who doesn't know the code. A good log message tells them what happened
and, ideally, what they can do about it, such as which file, resource, or
configuration to examine. That helps both sides. Users who can troubleshoot
their own systems don't need to ask for help, and the questions that do reach
the developers arrive with the right information attached.
Log what helps readers understand what's going on and why. What's noise in one
situation is a decisive hint in another, and the log level is what separates the
two. It decides who sees a message, and when.
For instance, a periodic task that finds nothing is noise at Info level, but
useful at Debug level. Pick the level based on what the reader is expected to
do with the message. As a rule of thumb, use Debug for details that only
matter when troubleshooting, Info for the normal course of events, Warn for
problems that k0s handles on its own, but that may need attention if they
persist, and Error for failures that require someone to act.
While retries are ongoing, log each failed attempt at Debug if failures are
expected or at Warn if they are not. Escalate to Error once retries are
exhausted or if the operation has not succeeded within a reasonable time. If the
consequences aren't obvious, explain what will happen, e.g. if an operation will
be retried, skipped, or abandoned.
Prefer putting variable data, especially error values, into fields rather than
into the message text, so that messages stay stable and searchable. Fields are
useful when the same name is used across messages because a filter on that name
finds every message about the same thing. Messages without placeholders work the
other way around. They read the same wherever they're logged, so filtering on
them finds all things affected by the same event. Values that no other message
would share are fine in the message text. Keep messages in sync with the
conditions they describe. When a condition changes, its message usually has to
change, too.
Unlike an error message, a log message stands on its own. Write it as a
sentence, starting with a capital letter and without a trailing period.
Log errors where they're handled, not where they're passed on. An error that's
both logged and returned will usually end up in the logs once for every layer it
passes through. When logging an error, avoid repeating what the error message
already says. The message provides context, while the error provides the cause.
Compatibility across versions
k0s is a per-node agent. Each node reads its own configuration at startup and
acts locally. Cluster-wide coordination is the responsibility of the users or of
additional tooling, such as Autopilot, k0sctl or k0smotron. Nodes are
upgraded individually rather than all at once, so controllers and workers of
adjacent versions run side by side for a while, within the limits of the
version skew policy.
Any change has to work under those conditions: a new version must interoperate
with the previous one, and it must keep reading whatever an earlier version has
persisted, on disk or in the cluster. The burden is on the newer version, since
the older one can't know anything about behavior that didn't exist yet. Whatever
it persists itself must either stay usable by the previous version, or be kept
apart from what the previous version reads, for example by versioning its name
or location.
Prefer designs that converge node by node to those that require every node to
flip simultaneously. If the old and new behaviors can't coexist, don't hide the
switch in the upgrade. Keep the old behavior until users explicitly enable the
new one so that they can choose when the disruption happens, and document what
the switch entails.
Changed configuration defaults must not alter the behavior of clusters that were
created with a previous version. New clusters get the new defaults, while
existing clusters keep behaving as before unless they opt in. This applies to
defaults that stand in for a choice users could have made in the configuration,
not to properties of the release itself, such as the versions of the components
that k0s ships.
Transitional code
Code that only exists to migrate from an earlier version or clean up leftovers
will be removed eventually. Keep it small and local, and mark it with a TODO
note indicating the version in which it can be removed. Don't build abstractions
or test suites around something scheduled for deletion unless the logic is
complex enough to justify the test. Verify it once and explain how in the pull
request.
Tests
Use testify for assertions, and add assertion messages that state the
expectation, so that a failure reads as a sentence. Test names describe the
behavior under test, not the reason the test was added. Use obviously made-up
values for test inputs that aren't the subject of the test, so that it's clear
which values matter and which don't. See the testing guide for how to run the
different kinds of tests, and what's expected before a pull request is merged.
Contributors Docs (`docs/contributors/docs.md`)
Contributing to the k0s documentation
We use mkdocs and mike for publishing docs to docs.k0sproject.io.
This guide will provide a simple how-to on how to configure and deploy newly added docs to our website.
Requirements
Install mike: https://github.com/jimporter/mike#installation
Adding A New link to the Navigation
- All docs must live under the
docsdirectory (I.E., changes to the mainREADME.mdare not reflected in the website). - Add a new link under
navin the main mkdocs.yml file:
# ... other directives
{%
include "../../mkdocs.yml"
start="# ~~~ START NAV SNIPPET ~~~\n"
end="\n# ~~~ END NAV SNIPPET ~~~\n"
%}
# more navigation links ...- Once your changes are pushed to
main, the "Publish Docs" job will start running: https://github.com/k0sproject/k0s/actions?query=workflow%3A%22Publish+docs+via+GitHub+Pages%22 - You should see the deployment outcome in the
gh-pagesdeployment page: https://github.com/k0sproject/k0s/deployments/activity_log?environment=github-pages
Testing docs locally
We've got a dockerized setup for easily testing docs locally. Simply runmake docs-serve-dev. The docs will be available on http://localhost:8000.
Note If you have something already running locally on port 8000 you can
choose another port like so: make docs-serve-dev DOCS_DEV_PORT=9999. The docs
will then be available on http://localhost:9999.
Contributors Github Workflow (`docs/contributors/github_workflow.md`)
GitHub Workflow
This guide assumes you have already cloned the upstream repo to your system via git clone, or via go get github.com/k0sproject/k0s.
Fork The Project
- Go to http://github.com/k0sproject/k0s
- On the top, right-hand side, click on "fork" and select your username for the fork destination.
Adding the Forked Remote
export GITHUB_USER={ your github username }cd $WORKDIR/k0s
git remote add $GITHUB_USER [email protected]:${GITHUB_USER}/k0s.git
# Prevent push to Upstream
git remote set-url --push origin no_push
# Set your fork remote as a default push target
git push --set-upstream $GITHUB_USER mainYour remotes should look something like this:
git remote -vorigin https://github.com/k0sproject/k0s (fetch)
origin no_push (push)
my_fork [email protected]:{ github_username }/k0s.git (fetch)
my_fork [email protected]:{ github_username }/k0s.git (push)Create & Rebase Your Feature Branch
Create a feature branch and switch to it:
git checkout -b my_feature_branchRebase your branch:
git fetch origin && \
git rebase origin/mainCurrent branch my_feature_branch is up to date.Please don't use git pull instead of the above fetch / rebase. git pull does a merge, which leaves merge commits. These make the commit history messy and violate the principle that commits ought to be individually understandable and useful.
Commit & Push
Each commit should serve one purpose. Keep unrelated changes, such as
refactorings or cleanups you make along the way, in separate commits. Commit
and sign your changes:
git commit --signoffCommit messages should have a short,
capitalized subject without trailing
period as first line. After the subject a
blank line and then a longer description
that explains why the change was
made, unless it is obvious.
Use imperative mood in the commit message.
For example:
Summarize changes in around 50 characters or less
More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as the
subject of the commit and the rest of the text as the body. The
blank line separating the summary from the body is critical (unless
you omit the body entirely); various tools like `log`, `shortlog`
and `rebase` can get confused if you run the two together.
Explain the problem that this commit is solving. Focus on why you
are making this change as opposed to how (the code explains that).
Are there side effects or other unintuitive consequences of this
change? Here's the place to explain them.
Further paragraphs come after blank lines.
- Bullet points are okay, too
- Typically a hyphen or asterisk is used for the bullet, preceded
by a single space, with blank lines in between.
Put references to earlier commits and other relevant material at the
bottom, like this:
Fixes: a50271db8 ("Implement k0s feature gates")
See: f9773809e ("Encapsulate debug flag handling")
See: https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/
Signed-off-by: Name Lastname <[email protected]>The commit message is read by whoever looks at this change years from now viagit blame or git log. Write it for them:
- State what was wrong, what the change does, and why. If the change fixes a
regression, say what the effect was in released versions. - Mention non-obvious constraints the code depends on, such as ordering,
timing, or compatibility with data written by earlier versions. These are
the things a future reader can't reconstruct from the diff. - If you remove dead code or do unrelated cleanup while you're there, say so.
Don't present it as part of the fix. - Leave out how you found the bug, what you tried, what the change does not
fix, and anything addressed to the reviewer. That material belongs in the
pull request description or the discussion. - Don't reference source file paths unless it aids understanding. The diff
already lists them.
Use Fixes: for the commit that introduced the problem this change corrects,
if it can be identified. The issue a change resolves is referenced from the
pull request description instead, where GitHub's closing keywords act on it.
Use See: for anything else that helps to understand the change: related
commits, external documentation, specifications, and the like.
When referencing other commits, use the abbreviated hash, e.g. as printed bygit log --oneline, along with the subject line: a50271db8 ("Implement k0s feature gates"). That way, the reference is meaningful in git log, whether or
not the hash resolves. In general, prefer what works locally over what only
works on GitHub.
Note that GitHub adds a timeline event to an issue or pull request whenever a
commit referencing it is pushed to any public repository, including your own
fork. Since a rebase creates new commits, every force-push repeats those events,
and frequent rebases leave a trail of identical entries in the timeline. That's
another reason to reference issues and pull requests from the pull request
description rather than from commit messages.
When ready, push your changes to your fork's repository:
git push --set-upstream my_fork my_feature_branchYou can go back and edit/build/test some more, then commit --amend in a few
cycles.
After review rounds, fold any fixup commits into the commits they belong to and
rewrite the messages so that each commit describes its final change, not its
history. Commits titled "Address review comments" are fine during reviews, but
clutter the history once merged. See Squashing Commits for
how to do that.
Open a Pull Request
See GitHub's docs on how to create a pull request from a fork.
Just as with commits, a pull request should serve one purpose. While a pull
request may have a broader goal than a single commit, each commit within it
should be a self-contained step toward that goal. Keep unrelated changes in
separate pull requests. Focused pull requests are easier to understand, review,
and manage. Contested but unrelated parts of a pull request might prevent other
acceptable parts from being merged, and backports become more manageable when
they don't have to be untangled from unrelated changes first.
The purpose of the pull request description is to facilitate discussion. It
should include the context, the alternatives considered, what was verified and
how it was verified, and what the change deliberately leaves out. It must not
contradict the commit messages. When you rewrite commits after a review round,
re-read the description and update it, too.
You may open a pull request in draft mode. Automated tests run against a draft
just like against any other pull request, but it will not be assigned for
review. Once a PR is ready for review, mark it as "Ready for review", and code
owners will be notified.
Get a code review
Once your pull request has been opened it will be assigned to one or more
reviewers, and will go through a series of smoke tests. It can be merged once
all automated checks pass and a code owner has approved it.
Commit changes made in response to review comments should be added to the same
branch on your fork. Reviews usually take a few rounds. Address feedback by
reworking the commits themselves, rather than by appending fixup commits, so
that each commit is still individually understandable. It's good practice to
rebase them on the current main branch while you're at it.
Very small PRs are easy to review. Very large PRs are very difficult to review.
Squashing Commits
Commits on your branch should represent meaningful milestones or units of work.
Small commits that contain typo fixes, rebases, review feedbacks, etc should be squashed.
To do that, it's best to perform an interactive rebase:
Example
Rebase your feature branch against upstream main branch:
git rebase -i origin/mainIf your PR has 3 commits, output would be similar to this:
pick f7f3f6d Changed some code
pick 310154e fixed some typos
pick a5f4a0d made some review changes
# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented outUse a command line text editor to change the word pick to f of fixup for the commits you want to squash, then save your changes and continue the rebase:
Per the output above, you can see that:
fixup <commit> = like "squash", but discard this commit's log messageWhich means that when rebased, the commit message "fixed some typos" will be removed, and squashed with the parent commit.
Push Your Final Changes
Once done, you can push the final commits to your branch:
git push --forceYou can run multiple iteration of rebase/push -f, if needed.
Contributors Testing (`docs/contributors/testing.md`)
Testing Your Code
k0s uses GitHub Actions to run automated tests on pull requests. The workflows
only run after a k0s maintainer has approved them, and a pull request won't be
merged until all tests are green. To save time and prevent your PR from going
stale, run the tests locally before submitting it.
Run Local Verifications
Please run the following style and formatting commands, and then fix and check
in any changes. These commands catch formatting and common mistakes. For
anything they can't catch, see the code guidelines.
Linting
We use
golangci-lintfor style verification.
In the repository's root directory, simply run:shellmake lintThere's no need to install
golangci-lintmanually. The build system will
take care of that.Go fmt
shellgo fmt ./...Checking the documentation
Follow the instructions for testing the documentation
locally to verify any changes.Pre-submit Flight Checks
In the repository root directory, make sure that:
make build && git diff --exit-coderuns successfully.
Verifies that the build is working and that the generated source code
matches the one that's checked into source control.make check-unitruns successfully.
Verifies that all the unit tests pass.make check-basicruns successfully.
Verifies basic cluster functionality using one controller and two workers.make check-hacontrolplaneruns successfully.
Verifies that joining of controllers works.
Please note that this last test is prone to "flakiness", so it might fail on
occasion. If it fails constantly, take a deeper look at your code to find the
source of the problem.If you find that all tests passed, you may open a pull request upstream.
Integration tests (a.k.a. inttests or smoketests)
The integration tests are located in the inttest directory and are run in CI as
"smoketests". These tests use Docker and bootloose-based nodes to
launch k0s clusters locally. See README.md.
Requirements
- Docker (Linux) with the ability to run privileged containers.
- Sufficient disk space for the bootloose images and test artifacts.
Running locally
You can run a single integration test by calling make from the repository
root:
make check-<name>Examples:
make check-basic
make check-ap-airgapSome tests, such as those related to airgap and IPv6, require image bundles. The
Makefile automatically builds and integrates those when you run make check-.... The underlying environment variables are:
K0S_IMAGES_BUNDLE(airgap bundle)K0S_EXTRA_IMAGES_BUNDLE(IPv6 bundle)
Debugging local failures
When a test fails, the suite writes logs and a support bundle to the temporary
directory (as determined by os.TempDir), e.g.:
/tmp/controller*.out.log,/tmp/controller*.err.log/tmp/worker*.out.log,/tmp/worker*.err.log/tmp/support-bundle.tar.gz
Start by inspecting the logs. You can also use sbctl with the support bundle
to inspect the state of the test cluster at the end of the test execution.
Debugging CI failures
When a CI run fails, the aforementioned files are uploaded and attached as job
artifacts. In the GitHub Actions UI, open the failed job, download and extract
the artifact for that test (named <smoketest-name>-<arch>-files or similar)
and extract it. You can then use the extracted files in the same way described
in the previous section.
Running CI on your fork
The k0s GitHub Action workflows use GitHub's hosted runners whenever possible.
This allows you to run the same CI workflows on your fork by pushing to the main
branch or opening a pull request on your fork. Ensure that GitHub Actions are
enabled for the forked repository. Note that forked repositories do not have
access to k0s's ARMv7 runners; therefore, all ARMv7-related workflow runs will
be skipped.
Running the CI on your fork may provide faster feedback than running it on a
pull request against the k0s repository because running the GitHub workflows for
pull requests requires approval from a k0s maintainer. Additionally, it allows
you to tinker with the tests and workflows however you like to debug things.
This could be useful if you cannot run the smoketests locally.
Cleanup the local workspace
In order to clean up the local workspace, run make clean. It will clean up all
of the intermediate files and directories created during the k0s build. Note
that you can't just use git clean -X or even rm -rf, since the Go modules
cache sets all of its subdirectories to read-only. If you get in trouble while
trying to delete your local workspace, try chmod -R u+w /path/to/workspace && rm -rf /path/to/workspace.
Examples Ansible Playbook (`docs/examples/ansible-playbook.md`)
Creating a cluster with an Ansible Playbook
Ansible is a popular infrastructure-as-code tool that can use to automate tasks for the purpose of achieving the desired state in a system. With Ansible (and the k0s-Ansible playbook) you can quickly install a multi-node Kubernetes Cluster.
Note: Before using Ansible to create a cluster, you should have a general understanding of Ansible (refer to the official Ansible User Guide.
Prerequisites
You will require the following tools to install k0s on local virtual machines:
| Tool | Detail |
|---|---|
multipass |
A lightweight VM manager that uses KVM on Linux, Hyper-V on Windows, and hypervisor.framework on macOS. Installation information |
ansible |
An infrastructure as code tool. Installation Guide |
kubectl |
Command line tool for running commands against Kubernetes clusters. Kubernetes Install Tools |
Create the cluster
Download k0s-ansible
Clone the k0s-ansible repository on your local machine:
shellgit clone https://github.com/movd/k0s-ansible.git cd k0s-ansibleCreate virtual machines
Note: Though multipass is the VM manager in use here, there is no interdependence.
Create a number of virtual machines. For the automation to work, each instance must have passwordless SSH access. To achieve this, provision each instance with a cloud-init manifest that imports your current users' public SSH key and into a user
k0s(refer to the bash script below).This creates 7 virtual machines:
shell./tools/multipass_create_instances.sh 7shellCreate cloud-init to import ssh key... [1/7] Creating instance k0s-1 with multipass... Launched: k0s-1 [2/7] Creating instance k0s-2 with multipass... Launched: k0s-2 [3/7] Creating instance k0s-3 with multipass... Launched: k0s-3 [4/7] Creating instance k0s-4 with multipass... Launched: k0s-4 [5/7] Creating instance k0s-5 with multipass... Launched: k0s-5 [6/7] Creating instance k0s-6 with multipass... Launched: k0s-6 [7/7] Creating instance k0s-7 with multipass... Launched: k0s-7 Name State IPv4 Image k0s-1 Running 192.168.64.32 Ubuntu 20.04 LTS k0s-2 Running 192.168.64.33 Ubuntu 20.04 LTS k0s-3 Running 192.168.64.56 Ubuntu 20.04 LTS k0s-4 Running 192.168.64.57 Ubuntu 20.04 LTS k0s-5 Running 192.168.64.58 Ubuntu 20.04 LTS k0s-6 Running 192.168.64.60 Ubuntu 20.04 LTS k0s-7 Running 192.168.64.61 Ubuntu 20.04 LTSCreate Ansible inventory
Copy the sample to create the inventory directory:
shellcp -rfp inventory/sample inventory/multipassCreate the inventory.
Assign the virtual machines to the different host groups, as required by the playbook logic.
Host group Detail initial_controllerMust contain a single node that creates the worker and controller tokens needed by the other nodes controllerCan contain nodes that, together with the host from initial_controller, form a highly available isolated control planeworkerMust contain at least one node, to allow for the deployment of Kubernetes objects Fill in
inventory/multipass/inventory.yml. This can be done by direct entry using the metadata provided bymultipass list,, or you can use the following Python scriptmultipass_generate_inventory.py:shell./tools/multipass_generate_inventory.pyshellDesignate first three instances as control plane Created Ansible Inventory at: /Users/dev/k0s-ansible/tools/inventory.yml $ cp tools/inventory.yml inventory/multipass/inventory.ymlYour
inventory/multipass/inventory.ymlshould resemble the example below:yaml--- all: children: initial_controller: hosts: k0s-1: controller: hosts: k0s-2: k0s-3: worker: hosts: k0s-4: k0s-5: k0s-6: k0s-7: hosts: k0s-1: ansible_host: 192.168.64.32 k0s-2: ansible_host: 192.168.64.33 k0s-3: ansible_host: 192.168.64.56 k0s-4: ansible_host: 192.168.64.57 k0s-5: ansible_host: 192.168.64.58 k0s-6: ansible_host: 192.168.64.60 k0s-7: ansible_host: 192.168.64.61 vars: ansible_user: k0s
Test the virtual machine connections
Run the following command to test the connection to your hosts:
shellansible -i inventory/multipass/inventory.yml -m pingshellk0s-4 | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "ping": "pong" } ...If the test result indicates success, you can proceed.
Provision the cluster with Ansible
Applying the playbook, k0s download and be set up on all nodes, tokens will be exchanged, and a kubeconfig will be dumped to your local deployment environment.
shellansible-playbook site.yml -i inventory/multipass/inventory.ymltext
/* Detailed source-code truncated for AI context efficiency. */
## Use the cluster with kubectl
A kubeconfig was copied to your local machine while the playbook was running which you can use to gain access to your new Kubernetes cluster:
```shell
export KUBECONFIG=/Users/dev/k0s-ansible/inventory/multipass/artifacts/k0s-kubeconfig.yml
kubectl cluster-infoKubernetes control plane is running at https://192.168.64.32:6443
CoreDNS is running at https://192.168.64.32:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://192.168.64.32:6443/api/v1/namespaces/kube-system/services/https:metrics-server:/proxy
$ kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k0s-4 Ready <none> 21s v1.20.1-k0s1 192.168.64.57 <none> Ubuntu 20.04.1 LTS 5.4.0-54-generic containerd://1.4.3
k0s-5 Ready <none> 21s v1.20.1-k0s1 192.168.64.58 <none> Ubuntu 20.04.1 LTS 5.4.0-54-generic containerd://1.4.3
k0s-6 NotReady <none> 21s v1.20.1-k0s1 192.168.64.60 <none> Ubuntu 20.04.1 LTS 5.4.0-54-generic containerd://1.4.3
k0s-7 NotReady <none> 21s v1.20.1-k0s1 192.168.64.61 <none> Ubuntu 20.04.1 LTS 5.4.0-54-generic containerd://1.4.3Note: The first three control plane nodes will not display, as the control plane is fully isolated. To check on the distributed etcd cluster, you can use ssh to securely log a controller node, or you can run the following ad-hoc command:
ansible k0s-1 -a "k0s etcd member-list -c /etc/k0s/k0s.yaml" -i inventory/multipass/inventory.yml | tail -1 | jq{
"level": "info",
"members": {
"k0s-1": "https://192.168.64.32:2380",
"k0s-2": "https://192.168.64.33:2380",
"k0s-3": "https://192.168.64.56:2380"
},
"msg": "done",
"time": "2020-12-23T00:21:22+01:00"
}Once all worker nodes are at Ready state you can use the cluster. You can test the cluster state by creating a simple nginx deployment.
kubectl create deployment nginx --image=gcr.io/google-containers/nginx --replicas=5deployment.apps/nginx createdkubectl expose deployment nginx --target-port=80 --port=8100service/nginx exposedkubectl run hello-k0s --image=quay.io/prometheus/busybox --rm -it --restart=Never --command -- wget -qO- nginx:8100<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx on Debian!</title>
...
pod "hello-k0s" deletedNote: k0s users are the developers of k0s-ansible. Please send your feedback, bug reports, and pull requests to github.com/movd/k0s-ansible._
Examples Gitops Flux (`docs/examples/gitops-flux.md`)
Using GitOps with Flux
This tutorial describes the benefits of using GitOps with k0s and provides an example of deploying an application with Flux v2.
GitOps is a practice where you leverage Git as the single source of truth. It offers a declarative way to do Kubernetes cluster management and application delivery. The desired states, using Kubernetes manifests and helm packages, are pulled from a git repository and automatically deployed to the cluster. This also makes it quick to re-deploy and recover applications whenever needed.
Why GitOps with k0s
k0s doesn't come with a lot of different extensions and add-ons that some users might find useful (and some not). Instead, k0s comes with 100% upstream Kubernetes and is compatible with all Kubernetes extensions. This makes it easy for k0s users to freely select the needed extensions that their applications and infrastructure need, without conflicting to any predefined options. Now, GitOps is a perfect practice to deploy these extensions automatically with applications by defining and configuring them directly in Git. This will also help with cluster security as the cluster doesn't need to be accessed directly when application changes are needed. However, this puts more stress on the Git access control, because changes in Git are propagated automatically to the cluster.
Install k0s
Let's start by installing k0s. Any k0s deployment option will do, but to keep things simple, this Quick Start Guide gets you started with a single node k0s cluster.
Run these three commands to download k0s, install and start it:
curl --proto '=https' --tlsv1.2 -sSf https://get.k0s.sh | sudo sh
sudo k0s install controller --single
sudo k0s startSet kubeconfig
Next, you need to set the KUBECONFIG variable, which is needed by Flux CLI later on.
sudo k0s kubeconfig admin > kubeconfig
export KUBECONFIG=$PWD/kubeconfigInstall Flux
To proceed with Flux, install the Flux CLI, which is used for configuring Flux to your Kubernetes cluster. For macOS and Linux, this can be done either with brew or bash script. Use one of them:
brew install fluxcd/tap/fluxor
curl --proto '=https' --tlsv1.2 -sSf https://fluxcd.io/install.sh | sudo bashFor more details of the Flux installation, check the Flux documentation.
Configure Flux for a GitHub repository
Export your GitHub personal access token (instructions how to get it) and username:
export GITHUB_TOKEN=<your-token>
export GITHUB_USER=<your-username>Come up with a GitHub repo name (e.g. flux-demo), which will be used by Flux to store (and sync) the config files.
export GITHUB_REPO_NAME=<select-repo-name-to-be-created>Bootstrap flux to your cluster. The GitHub repo will be created automatically by Flux:
flux bootstrap github \
--owner=$GITHUB_USER \
--repository=$GITHUB_REPO_NAME \
--branch=main \
--path=./clusters/my-cluster \
--personalNow you are all set with Flux and can proceed to deploy your first application.
Deploy example application
Next, we'll deploy a simple web application and expose it using a NodePort service. In the previous step, we configured Flux to track the path /clusters/my-cluster/ in your repository. Now clone the repo to your local machine:
git clone [email protected]:$GITHUB_USER/$GITHUB_REPO_NAME.git
cd $GITHUB_REPO_NAME/clusters/my-cluster/Create the following YAML file (simple-web-server-with-nodeport.yaml) into the same directory:
apiVersion: v1
kind: Namespace
metadata:
name: web
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-server
namespace: web
spec:
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: httpd
image: httpd:2.4.53-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: web-server-service
namespace: web
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 80
nodePort: 30003Then push the new file to the repository:
git add .
git commit -m "Add web server manifest"
git pushCheck that Flux detects your changes and the web server gets applied (by default this should happen within 1 min):
flux get kustomizationsIf the deployment went successfully, you should see the newly added objects:
sudo k0s kubectl get all -n webYou can try to access the web application using
curl localhost:30003or by using a web browser http://localhost:30003.
Voilà! You have now installed the example application using the GitOps method with Flux. As a next step you can try to modify the web app YAML file or add another application directly in to the Git repo and see how Flux will automatically pick up the changes without accessing the cluster with kubectl.
Uninstall Flux
If you want to uninstall Flux from the cluster, run:
flux uninstall --namespace=flux-systemYour applications, which were installed by Flux, will remain in the cluster, but you don't have the Flux processes anymore to sync up the desired state from Git.
Examples Longhorn (`docs/examples/longhorn.md`)
Longhorn
This tutorial covers installing Longhorn as a Helm extension on k0s. Longhorn is a distributed block storage system for Kubernetes that provides enterprise-grade features including:
- Distributed storage: Provides persistent storage across your cluster nodes
- Snapshots and backups: Built-in snapshot and backup capabilities for data protection
- Live upgrades: Zero-downtime upgrades of storage volumes
- High availability: Automatic failover and replication for data durability
- Cross-cluster disaster recovery: Backup to external storage systems
Longhorn is particularly well-suited for stateful applications that require persistent storage, such as databases, message queues, and content management systems.
For more information on Longhorn, refer to the official documentation.
Prerequisites
Before installing Longhorn, ensure your k0s cluster meets these requirements:
System Requirements
Please refer to the official Longhorn v1.10.x Support Matrix for detailed system requirements.
Required Software
Longhorn requires the iSCSI initiator utilities to be installed on every node in your cluster:
Why iSCSI is required: Longhorn uses iSCSI protocol to provide block storage to pods. The iSCSI initiator allows nodes to connect to Longhorn volumes as if they were local block devices.
Ubuntu/Debian:
sudo apt-get install -y open-iscsiRHEL/CentOS/Rocky:
sudo yum install -y iscsi-initiator-utilsSUSE:
sudo zypper install -y open-iscsiEnsure the iSCSI daemon is enabled and running:
sudo systemctl enable --now iscsidInstall via k0s Helm extension
Add Longhorn as a Helm chart in your k0s ClusterConfig. Apply this configuration through your normal k0s configuration workflow.
Basic Installation
The simplest way to install Longhorn is to add it to your k0s ClusterConfig:
apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig
metadata:
name: k0s
spec:
extensions:
helm:
repositories:
- name: longhorn
url: https://charts.longhorn.io
charts:
- name: longhorn
chartname: longhorn/longhorn
namespace: longhorn-system
version: 1.10.1Verification Steps
After applying the configuration, verify that Longhorn is properly installed:
Check pod status: Wait for all Longhorn components to become ready:
k0s kubectl -n longhorn-system get podsExpected output should show all pods in Running status. This may take 2-3 minutes.
Verify StorageClass: Confirm that the longhorn StorageClass was created:
$ k0s kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
longhorn (default) driver.longhorn.io Delete Immediate true 16m
longhorn-static driver.longhorn.io Delete Immediate true 16mYou should see longhorn listed as a storage class.
Single-Node Cluster Configuration
Single-node clusters require setting the default replica count to 1 to prevent scheduling failures, as Longhorn's default of 3 replicas cannot be satisfied on a single node. This setting can be adjusted in the values or post-installation via the UI (Settings > General > Default Replica Count).
spec:
extensions:
helm:
charts:
- name: longhorn
chartname: longhorn/longhorn
namespace: longhorn-system
version: "latest"
values: |
defaultSettings:
defaultReplicaCount: 1Access the Longhorn UI
You can port-forward the frontend service:
k0s kubectl -n longhorn-system port-forward svc/longhorn-frontend 8080:80Then open http://localhost:8080 in your browser.
Example: PVC and Deployment
Use this example to create a PVC and mount it in a Deployment using Longhorn.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-pvc
namespace: default
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: default
labels:
app: nginx
spec:
selector:
matchLabels:
app: nginx
strategy:
type: Recreate
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
volumeMounts:
- name: persistent-storage
mountPath: /var/lib/nginx
volumes:
- name: persistent-storage
persistentVolumeClaim:
claimName: app-pvcApply the manifest:
k0s kubectl apply -f nginx.yamlCheck the PVC and pod status:
k0s kubectl get pvc,podsNext Steps
Longhorn supports backing up volumes to external storage systems such as S3, NFS, or CIFS. To enable backups:
- Create a backup target secret with your storage credentials
- Configure the backup target URL in Longhorn settings
- Set up recurring backup jobs for automatic protection
For detailed instructions on configuring backup targets and setting up automated backups, refer to the Longhorn Backup Documentation.
Additional Resources
- Longhorn Official Documentation
- Longhorn Architecture Guide
- Best Practices for Production Deployments
- Troubleshooting Guide
Examples Metallb Loadbalancer (`docs/examples/metallb-loadbalancer.md`)
Installing MetalLB Load Balancer
This tutorial covers the installation of MetalLB load balancer on k0s. k0s doesn't come with an in-built load balancer, but it's easy to deploy MetalLB as shown in this document.
About Load Balancers
Load balancers can be used for exposing applications to the external network. Load balancer provides a single IP address to route incoming requests to your app. In order to successfully create Kubernetes services of type LoadBalancer, you need to have the load balancer (implementation) available for Kubernetes.
Load balancer can be implemented by a cloud provider as an external service (with additional cost). This can also be implemented internally in the Kubernetes cluster (pure SW solution) with MetalLB.
MetalLB
MetalLB implements the Kubernetes service of type LoadBalancer. When a LoadBalancer service is requested, MetalLB allocates an IP address from the configured range and makes the network aware that the IP “lives” in the cluster.
One of the benefits of MetalLB is that you avoid all cloud provider dependencies. That's why MetalLB is typically used for bare-metal deployments.
See the MetalLB requirements in the MetalLB's official documentation. By default, k0s runs with Kube-Router CNI, which is compatible with MetalLB as long as you don't use MetalLB’s BGP mode. If you are not using Kube-Router and you are using kube-proxy in IPVS mode, you need to enable strict ARP mode in kube-proxy (see MetalLB preparations):
apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig
metadata:
name: k0s
spec:
network:
kubeProxy:
mode: ipvs
ipvs:
strictARP: truePort 7946 (TCP & UDP) must be allowed between the nodes. In addition, before installing MetalLB, make sure there is no other software running on port 7946 on the nodes, such as docker daemon.
Install MetalLB
Install MetalLB using the official Helm chart and k0s Helm extension manager:
yamlapiVersion: k0s.k0sproject.io/v1beta1 kind: ClusterConfig metadata: name: k0s spec: extensions: helm: repositories: - name: metallb url: https://metallb.github.io/metallb charts: - name: metallb chartname: metallb/metallb version: "0.15.3" namespace: metallb-systemOther installation methods are available in the MetalLB's official documentation.
Create ConfigMap for MetalLB
Next you need to create ConfigMap, which includes an IP address range for the load balancer. The pool of IPs must be dedicated to MetalLB's use. You can't reuse for example the Kubernetes node IPs or IPs controlled by other services. You can, however, use private IP addresses, for example 192.168.1.180-192.168.1.199, but then you need to take care of the routing from the external network if you need external access. In this example, we don't need it.
Create a YAML file accordingly, and deploy it:
kubectl apply -f metallb-l2-pool.yamlyaml--- apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: first-pool namespace: metallb-system spec: addresses: - <ip-address-range-start>-<ip-address-range-stop> # or - <ip-address>/<cidr> --- apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: example namespace: metallb-systemDeploy an example application (web server) with a load balancer
yamlapiVersion: v1 kind: Namespace metadata: name: web --- apiVersion: apps/v1 kind: Deployment metadata: name: web-server namespace: web spec: selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: - name: httpd image: httpd:2.4.53-alpine ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: web-server-service namespace: web spec: selector: app: web ports: - protocol: TCP port: 80 targetPort: 80 type: LoadBalancerCheck your LoadBalancer
Run the following command to see your LoadBalancer with the external-ip and port.
shellkubectl get service -n webAccess your example application
If you used private IP addresses for MetalLB in the ConfigMap (in step 2), you should run the following command from the local network. Use the IP address from the previous step.
shellcurl <EXTERNAL-IP>If you are successful, you should see
<html><h1>It works!</h1></html>.
Additional information
For more information about MetalLB installation, take a look at the official MetalLB documentation.
Alternative examples
Get load balancer using cloud provider.
Examples Nginx Ingress (`docs/examples/nginx-ingress.md`)
Installing NGINX Ingress Controller
This tutorial covers the installation of Ingress NGINX Controller, which is an
open source project made by the Kubernetes community. k0s doesn't come with a
built-in ingress controller, but it's easy to deploy Ingress NGINX Controller as
shown in this document. Other ingress solutions can be used as well (see the
links at the end of the page).
`NodePort` vs. `LoadBalancer` vs. `Ingress`
Kubernetes offers multiple options for exposing services to external networks. The main options are NodePort, LoadBalancer and Ingress.
NodePort, as the name says, means that a port on a node is configured to route incoming requests to a certain service. The port range is limited to 30000-32767, so you cannot expose commonly used ports like 80 or 443 via NodePort.
LoadBalancer is a service, which is typically implemented by the cloud provider as an external service (with additional cost). Load balancers can also be installed internally in the Kubernetes cluster with MetalLB, which is typically used for bare-metal deployments. Load balancer provides a single IP address to access your services, which can run on multiple nodes.
Ingress controller helps to consolidate routing rules of multiple applications into one entity. Ingress controller is exposed to an external network with the help of NodePort, LoadBalancer or host network. You can also use Ingress controller to terminate TLS for your domain in one place, instead of terminating TLS for each application separately.
Ingress NGINX Controller
Ingress NGINX Controller is a very popular ingress for Kubernetes. In many cloud environments, it can be exposed to an external network by using the load balancer offered by the cloud provider. However, cloud load balancers are not necessary. Load balancer can also be implemented with MetalLB, which can be deployed in the same Kubernetes cluster. Another option to expose the Ingress controller to an external network is to use NodePort. The third option is to use host network. All of these alternatives are described in more detail on below, with separate examples.
Installation using `NodePort`
Installing using NodePort is the most simple example for Ingress NGINX Controller as we can avoid the load balancer dependency. NodePort is used for exposing the controller to the external network.
Install Ingress NGINX Controller (using the official manifests from the official
ingress-nginxKubernetes git repository)shellkubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.1.3/deploy/static/provider/baremetal/deploy.yamlCheck that the Ingress controller pods have started
shellkubectl get pods -n ingress-nginxCheck that you can see the NodePort service
shellkubectl get services -n ingress-nginxFrom version
v1.0.0of the Ingress NGINX Controller, a ingressclass object is required.In the default installation, an ingressclass object named
nginxhas already been created.shell$ kubectl -n ingress-nginx get ingressclasses NAME CONTROLLER PARAMETERS AGE nginx k8s.io/ingress-nginx <none> 162mIf this is only instance of the Ingress NGINX Controller, you should add the annotation
ingressclass.kubernetes.io/is-default-classin your ingress class:shellkubectl -n ingress-nginx annotate ingressclasses nginx ingressclass.kubernetes.io/is-default-class="true"Try connecting the controller using the
NodePortfrom the previous step (in the range of 30000-32767)shellcurl <worker-external-ip>:<node-port>If you don't yet have any backend service configured, you should see "404 Not Found" from nginx. This is okay for now. If you see a response from nginx, the ingress controller is running and you can reach it.
Deploy a small test application (Apache HTTP Server) to verify your ingress controller.
Create the following YAML file and name it
simple-web-server-with-ingress.yaml:yamlapiVersion: v1 kind: Namespace metadata: name: web --- apiVersion: apps/v1 kind: Deployment metadata: name: web-server namespace: web spec: selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: - name: httpd image: httpd:2.4.53-alpine ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: web-server-service namespace: web spec: selector: app: web ports: - protocol: TCP port: 5000 targetPort: 80 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: web-server-ingress namespace: web spec: ingressClassName: nginx rules: - host: web.example.com http: paths: - path: / pathType: Prefix backend: service: name: web-server-service port: number: 5000Deploy the app:
shellkubectl apply -f simple-web-server-with-ingress.yamlVerify that you can access your application using the
NodePortfrom step 3.shellcurl <worker-external-ip>:<node-port> -H 'Host: web.example.com'If you are successful, you should see
<html><body><h1>It works!</h1></body></html>.
Install using `LoadBalancer`
In this example you'll install Ingress NGINX Controller using LoadBalancer on k0s.
Install a
LoadBalancerThere are two alternatives to install
LoadBalanceron k0s. Follow the links in order to installLoadBalancer.- MetalLB as a pure software solution running internally in the k0s cluster
- Cloud provider's load balancer running outside of the k0s cluster
Verify
LoadBalancerIn order to proceed you need to have a load balancer available for the Kubernetes cluster. To verify that it's available, deploy a simple Service of type
LoadBalancer.yamlapiVersion: v1 kind: Service metadata: name: example-load-balancer spec: selector: app: web ports: - protocol: TCP port: 80 targetPort: 80 type: LoadBalancershellkubectl apply -f example-load-balancer.yamlThen run the following command to see your
LoadBalancerwith an external IP address.shellkubectl get service example-load-balancerIf the
LoadBalanceris not available, you won't get an IP address for EXTERNAL-IP. Instead, it's<pending>. In this case you should go back to the previous step and check your load balancer availability.If you are successful, you'll see a real IP address and you can proceed further.
You can delete the example-load-balancer:
shellkubectl delete -f example-load-balancer.yamlInstall Ingress NGINX Controller by following the steps in the previous chapter (step 1 to step 4).
Edit the Ingress NGINX Controller to use
LoadBalancerinstead ofNodePortshellkubectl edit service ingress-nginx-controller -n ingress-nginxFind the spec.type field and change it from
NodePorttoLoadBalancer.Check that you can see the
ingress-nginxservice with typeLoadBalancer.shellkubectl get services -n ingress-nginxTry connecting to the ingress controller
If you used private IP addresses for MetalLB in step 2, you should run the following command from the local network. Use the IP address from the previous step, column EXTERNAL-IP.
shellcurl <EXTERNAL-IP>If you don't yet have any backend service configured, you should see "404 Not Found" from nginx. This is okay for now. If you see a response from nginx, the Ingress Controller is running and you can reach it using
LoadBalancer.Deploy a small test application (Apache HTTP Server) to verify your
Ingress.Create the YAML file
simple-web-server-with-ingress.yamlas described in the previous chapter (step 6) and deploy it.shellkubectl apply -f simple-web-server-with-ingress.yamlVerify that you can access your application through the
LoadBalancerand ingress controller.shellcurl <worker-external-ip> -H 'Host: web.example.com'If you are successful, you should see
<html><body><h1>It works!</h1></body></html>.
Install using host network
The host network option exposes Ingress directly using the worker nodes' IP addresses. It also allows you to use ports 80 and 443. This option doesn't use any Service objects (ClusterIP, NodePort, LoadBalancer) and it has the limitation that only one Ingress controller Pod may be scheduled on each cluster node.
Install Ingress NGINX Controller manifests:
shellwget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.1.3/deploy/static/provider/baremetal/deploy.yamlEdit
deploy.yaml. Find the Deployment ingress-nginx-controller and enable the host network option by adding thehostNetworkline:shellspec: template: spec: hostNetwork: trueYou can also remove the Service ingress-nginx-controller completely, because it won't be needed.
Install Ingress
shellkubectl apply -f deploy.yamlTry to connect to the ingress controller, deploy a test application and verify the access. These steps are similar to the previous install methods.
Additional information
For more information about Ingress NGINX Controller installation, take a look at
the official installation guide and bare-metal considerations.
Alternative examples for ingress controllers on k0s
Examples Openebs (`docs/examples/openebs.md`)
OpenEBS
This tutorial covers the installation of OpenEBS as a Helm extension. OpenEBS
can be installed as a helm chart by adding it as an extension to your
configuration.
Configuration
Most options can be directly set via the values section of the chart config.
Kubelet path
Be sure to set the correct kubelet path as the default one from the chart is /var/lib/kubelet, but k0s uses /var/lib/k0s/kubelet.
You can skip this if you set a custom directory via --kubelet-root-dir.
More infos in the OpenEBS Quickstart Guide.
lvm-localpv:
lvmNode:
kubeletDir: /var/lib/k0s/kubelet
zfs-localpv:
zfsNode:
kubeletDir: /var/lib/k0s/kubelet
mayastor:
csi:
node:
kubeletDir: /var/lib/k0s/kubeletStorage engines
Each storage engine can be individually enabled or disabled.
The default of the chart is to enable all engines except "rawfile".
If you want to disable replicated storage via mayastor, check the example below.
You can always check the default values here: https://github.com/openebs/openebs/blob/helm-testing/release/4.4/charts/values.yaml
engines:
local:
lvm:
enabled: true
rawfile:
enabled: true
zfs:
enabled: true
rawfile:
enabled: false
replicated:
mayastor:
enabled: trueMonitoring
Per default the chart also installs Loki, Minio and Alloy for logging purposes.
If you do not need these you can disable them.
alloy:
enabled: false
loki:
enabled: false
minio:
enabled: falseLocal storage only variant
extensions:
helm:
repositories:
- name: openebs-internal
url: https://openebs.github.io/openebs
charts:
- name: openebs
chartname: openebs-internal/openebs
version: "4.4.0"
namespace: openebs
order: 1
values: |
engines:
replicated:
mayastor:
enabled: false
lvm-localpv:
lvmNode:
kubeletDir: /var/lib/k0s/kubelet
zfs-localpv:
zfsNode:
kubeletDir: /var/lib/k0s/kubelet
localpv-provisioner:
hostpathClass:
isDefaultClass: falseLocal + replicated storage variant
Be sure to read the prerequisites of OpenEBS for distributed storage, as some host dependencies are needed.
extensions:
helm:
repositories:
- name: openebs-internal
url: https://openebs.github.io/openebs
charts:
- name: openebs
chartname: openebs-internal/openebs
version: "4.4.0"
namespace: openebs
order: 1
values: |
lvm-localpv:
lvmNode:
kubeletDir: /var/lib/k0s/kubelet
zfs-localpv:
zfsNode:
kubeletDir: /var/lib/k0s/kubelet
mayastor:
csi:
node:
kubeletDir: /var/lib/k0s/kubelet
# uncomment this to only use 1 cpu per io-engine (for smaller workloads)
# io_engine:
# cpuCount: "1"
localpv-provisioner:
hostpathClass:
isDefaultClass: falseSet default storage class
If you want OpenEBS "hostpath" to be your default storage class, set isDefaultClass to true.
Usage
Once installed, the cluster will have multiple storage classes available for you to use (depending on the enabled engines).
k0s kubectl get storageclassNAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
mayastor-etcd-localpv openebs.io/local Delete WaitForFirstConsumer false 76m
openebs-hostpath (default) openebs.io/local Delete WaitForFirstConsumer false 76m
# and depending on the enabled storage providers e.g.:
openebs-single-replica io.openebs.csi-mayastor Delete Immediate true 76mThe openebs-hostpath is the storage class that maps to /var/openebs/local.
The openebs-device is not configured and could be configured by manifest deployer accordingly to the OpenEBS documentation
Example
Use following manifests as an example of pod with mounted volume:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nginx-pvc
namespace: default
spec:
accessModes:
- ReadWriteOnce
storageClassName: openebs-hostpath
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: default
labels:
app: nginx
spec:
selector:
matchLabels:
app: nginx
strategy:
type: Recreate
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
volumeMounts:
- name: persistent-storage
mountPath: /var/lib/nginx
volumes:
- name: persistent-storage
persistentVolumeClaim:
claimName: nginx-pvck0s kubectl apply -f nginx.yamlpersistentvolumeclaim/nginx-pvc created
deployment.apps/nginx created
bash-5.1# k0s kc get pods
NAME READY STATUS RESTARTS AGE
nginx-d95bcb7db-gzsdt 1/1 Running 0 30sk0s kubectl get pvNAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-9a7fae2d-eb03-42c3-aaa9-1a807d5df12f 5Gi RWO Delete Bound default/nginx-pvc openebs-hostpath 30sExamples Rook Ceph (`docs/examples/rook-ceph.md`)
Installing Ceph Storage with Rook
In this tutorial you'll create a Ceph storage for k0s. Ceph is a highly scalable, distributed storage solution. It offers object, block, and file storage, and it's designed to run on any common hardware. Ceph implements data replication into multiple volumes that makes it fault-tolerant. Another clear advantage of Ceph in Kubernetes is the dynamic provisioning. This means that applications just need to request the storage (persistent volume claim) and Ceph will automatically provision the requested storage without a manual creation of the persistent volume each time.
Unfortunately, the Ceph deployment as such can be considered a bit complex. To make the deployment easier, we'll use Rook operator. Rook is a CNCF project and it's dedicated to storage orchestration. Rook supports several storage solutions, but in this tutorial we will use it to manage Ceph.
This tutorial uses three worker nodes and one controller. It's possible to use less nodes, but using three worker nodes makes it a good example for deploying a high-available storage cluster. We use external storage partitions, which are assigned to the worker nodes to be used by Ceph.
After the Ceph deployment we'll deploy a sample application (MongoDB) to use the storage in practice.
Prerequisites
- Linux
- GitHub access
- AWS account
- Terraform
Deployment steps
1. Preparations
In this example we'll use Terraform to create four Ubuntu virtual machines on AWS. Using Terraform makes the VM deployment fast and repeatable. You can avoid manually setting up everything in the AWS GUI. Moreover, when you have finished with the tutorial, it's very easy to tear down the virtual machines with Terraform (with one command). However, you can set up the nodes in many different ways and it doesn't make a difference in the following steps.
We will use k0sctl to create the k0s cluster. The k0sctl repository also includes a ready-made Terraform configuration to create the virtual machines on AWS. We'll use that. Let's start be cloning the k0sctl repo.
git clone [email protected]:k0sproject/k0sctl.gitTake a look at the Terraform files
cd k0sctl/examples/aws-tf
ls -lOpen variables.tf and set the number of controller and worker nodes like this:
variable "cluster_name" {
type = string
default = "k0sctl"
}
variable "controller_count" {
type = number
default = 1
}
variable "worker_count" {
type = number
default = 3
}
variable "cluster_flavor" {
type = string
default = "t3.small"
}Open main.tf to check or modify k0s version near the end of the file.
You can also configure a different name to your cluster and change the default VM type. t3.small (2 vCPUs, 2 GB RAM) runs just fine for this tutorial.
2. Create the virtual machines
For AWS, you need an account. Terraform will use the following environment variable: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN. You can easily copy-paste them from the AWS portal. For more information, see the AWS documentation.
When the environment variables are set, you can proceed with Terraform and deploy the virtual machines.
terraform init
terraform applyIf you decide to create the virtual machines manually using AWS GUI, you need to disable source / destination checking. This needs to be disabled always for multi-node Kubernetes clusters in order to get the node-to-node communication working due to Network Address Translation. For Terraform this is already taken care of in the default configuration.
3. Create and attach the volumes
Ceph requires one of the following storage options for storing the data:
- Raw devices (no partitions or formatted filesystems)
- Raw partitions (no formatted filesystem)
- PVs available from a storage class in block mode
We will be using raw partitions (AWS EBS volumes), which can be easily attached to the worker node virtual machines. They are automatically detected by Ceph with its default configuration.
Deploy AWS EBS volumes, one for each worker node. You can manually create three EBS volumes (for example 10 GB each) using the AWS GUI and attach those to your worker nodes. Formatting shouldn't be done. Instead, Ceph handles that part automatically.
After you have attached the EBS volumes to the worker nodes, log in into one of the workers and check the available block devices:
lsblk -fNAME FSTYPE LABEL UUID FSAVAIL FSUSE% MOUNTPOINT
loop0 squashfs 0 100% /snap/amazon-ssm-agent/3552
loop1 squashfs 0 100% /snap/core18/1997
loop2 squashfs 0 100% /snap/snapd/11588
loop3 squashfs 0 100% /snap/lxd/19647
nvme0n1
└─nvme0n1p1 ext4 cloudimg-rootfs e8070c31-bfee-4314-a151-d1332dc23486 5.1G 33% /
nvme1n1The last line (nvme1n1) in this example printout corresponds to the attached EBS volume. Note that it doesn't have any filesystem (FSTYPE is empty). This meets the Ceph storage requirements and you are good to proceed.
4. Install k0s using k0sctl
You can use Terraform to automatically output a config file for k0sctl with the IP addresses and access details.
terraform output -raw k0s_cluster > k0sctl.yamlAfter that deploying k0s becomes very easy with the ready-made configuration.
k0sctl apply --config k0sctl.yamlIt might take around 2-3 minutes for k0sctl to connect each node, install k0s and connect the nodes together to form a cluster.
5. Access k0s cluster
To access your new cluster remotely, you can use k0sctl to fetch kubeconfig and use that with kubectl or Lens.
k0sctl kubeconfig --config k0sctl.yaml > kubeconfig
export KUBECONFIG=$PWD/kubeconfig
kubectl get nodesThe other option is to login to your controller node and use the k0s in-built kubectl to access the cluster. Then you don't need to worry about kubeconfig (k0s takes care of that automatically).
ssh -i aws.pem <username>@<ip-address>
sudo k0s kubectl get nodes6. Deploy Rook
To get started with Rook, let's first clone the Rook GitHub repo:
git clone --single-branch --branch release-1.7 https://github.com/rook/rook.git
cd rook/cluster/examples/kubernetes/cephWe will use mostly the default Rook configuration. However, k0s kubelet directory must be configured in operator.yaml like this
ROOK_CSI_KUBELET_DIR_PATH: "/var/lib/k0s/kubelet"To create the resources, which are needed by the Rook’s Ceph operator, run
kubectl apply -f crds.yaml -f common.yaml -f operator.yamlNow you should see the operator running. Check them with
kubectl get pods -n rook-ceph7. Deploy Ceph Cluster
Then you can proceed to create a Ceph cluster. Ceph will use the three EBS volumes attached to the worker nodes:
kubectl apply -f cluster.yamlIt takes some minutes to prepare the volumes and create the cluster. Once this is completed you should see the following output:
kubectl get pods -n rook-ceph/* Detailed source-code truncated for AI context efficiency. */8. Configure Ceph block storage
Before Ceph can provide storage to your cluster, you need to create a ReplicaPool and a StorageClass. In this example, we use the default configuration to create the block storage.
kubectl apply -f ./csi/rbd/storageclass.yaml9. Request storage
Create a new manifest file mongo-pvc.yaml with the following content:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mongo-pvc
spec:
storageClassName: rook-ceph-block
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2GiThis will create Persistent Volume Claim (PVC) to request a 2 GB block storage from Ceph. Provisioning will be done dynamically. You can define the block size freely as long as it fits to the available storage size.
kubectl apply -f mongo-pvc.yamlYou can now check the status of your PVC:
kubectl get pvcWhen the PVC gets the requested volume reserved (bound), it should look like this:
kubectl get pvcNAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
mongo-pvc Bound pvc-08337736-65dd-49d2-938c-8197a8871739 2Gi RWO rook-ceph-block 6s10. Deploy an example application
Let's deploy a MongoDB database to verify the Ceph storage. Create a new file mongo.yaml with the following content:
apiVersion: apps/v1
kind: Deployment
metadata:
name: mongo
spec:
selector:
matchLabels:
app: mongo
template:
metadata:
labels:
app: mongo
spec:
containers:
- image: mongo:4.0
name: mongo
ports:
- containerPort: 27017
name: mongo
volumeMounts:
- name: mongo-persistent-storage
mountPath: /data/db
volumes:
- name: mongo-persistent-storage
persistentVolumeClaim:
claimName: mongo-pvcDeploy the database:
kubectl apply -f mongo.yaml11. Access the application
Open the MongoDB shell using the MongoDB pod:
kubectl get podsNAME READY STATUS RESTARTS AGE
mongo-b87cbd5cc-4wx8t 1/1 Running 0 76skubectl exec -it mongo-b87cbd5cc-4wx8t -- mongoCreate a DB and insert some data:
> use testDB
switched to db testDB
> db.testDB.insertOne( {name: "abc", number: 123 })
{
"acknowledged" : true,
"insertedId" : ObjectId("60815690a709d344f83b651d")
}
> db.testDB.insertOne( {name: "bcd", number: 234 })
{
"acknowledged" : true,
"insertedId" : ObjectId("6081569da709d344f83b651e")
}Read the data:
> db.getCollection("testDB").find()
{ "_id" : ObjectId("60815690a709d344f83b651d"), "name" : "abc", "number" : 123 }
{ "_id" : ObjectId("6081569da709d344f83b651e"), "name" : "bcd", "number" : 234 }
>You can also try to restart the MongoDB pod or restart the worker nodes to verity that the storage is persistent.
12. Clean-up
You can use Terraform to take down the virtual machines:
terraform destroyRemember to delete the EBS volumes separately.
Conclusions
You have now created a replicated Ceph storage for k0s. All your data is stored to multiple disks at the same time so you have a fault-tolerant solution. You also have enabled dynamic provisioning. Your applications can request the available storage without a manual creation of the persistent volumes each time.
This was just one example to deploy distributed storage to k0s cluster using an operator. You can easily use different Kubernetes storage solutions with k0s.
Examples Traefik Ingress (`docs/examples/traefik-ingress.md`)
Installing Traefik Ingress Controller
You can configure k0s with the Traefik Ingress Controller, a MetalLB service load balancer, and deploy the Traefik Dashboard using a service sample. To do this you leverage Helm's extensible bootstrapping functionality to add the correct extensions to the k0s.yaml file during cluster configuration.
Install Traefik and MetalLB
Configure k0s to install Traefik and MetalLB during cluster bootstrapping by adding their Helm charts as extensions in the k0s configuration file (k0s.yaml).
Note:
A good practice is to have a small range of IP addresses that are addressable on your network, preferably outside the assignment pool your DHCP server allocates (though any valid IP range should work locally on your machine). Providing an addressable range allows you to access your load balancer and Ingress services from anywhere on your local network.
extensions:
helm:
repositories:
- name: traefik
url: https://traefik.github.io/charts
- name: metallb
url: https://metallb.github.io/metallb
charts:
- name: traefik
chartname: traefik/traefik
version: "39.0.1"
namespace: traefik-system
- name: metallb
chart: metallb/metallb
version: "0.15.3"
namespace: metallb-systemConfigure traefik
If you want to configure Traefik via the Helm Chart, you can do this by providing values in the extensions.helm.charts section. For more infos check out the Helm Chart docs.
Enable automatic http to https redirect
To automatically redirect http requests to https, set the following config via the values section:
extensions:
helm:
repositories: ...
charts:
- name: traefik
chartname: traefik/traefik
version: "39.0.1"
namespace: traefik-system
values: |
ports:
web:
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
- name: metallb
...Add new entrypoints
Traefik can be configured to add new entrypoints which serve to accept traffic via a specified port. This can either be via the http(s) load-balancer, or the tcp/udp load-balancer.
For detailled infos check out the official docs.
To add a new entrypoint which can be used by IngressRoute or IngressRouteTCP/IngressRouteUDP CRD, set the following config via the values section:
extensions:
helm:
repositories: ...
charts:
- name: traefik
chartname: traefik/traefik
version: "39.0.1"
namespace: traefik-system
values: |
ports:
ssh: # name of the entrypoint
port: 2222 # port inside the container
expose:
default: true
exposedPort: 22 # port of the load-balancer which is accessed externally
protocol: TCP # protocol
- name: metallb
...Create ConfigMap for MetalLB
Next you need to create ConfigMap, which includes an IP address range for the load balancer. The pool of IPs must be dedicated to MetalLB's use. You can't reuse for example the Kubernetes node IPs or IPs controlled by other services.
For detailed infos about the installation and configuration of MetalLB see the Extension docs
Create a YAML file accordingly, and deploy it: kubectl apply -f metallb-l2-pool.yaml
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- <ip-address-range-start>-<ip-address-range-stop>
- <ip-address>/<cidr>
# example for a range
- 192.168.0.1-192.168.0.5
# example for a single address with cidr
- 192.168.0.5/32
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb-systemRetrieve the Load Balancer IP
After you start your cluster, run kubectl get all to confirm the deployment of Traefik and MetalLB. The command should return a response with the metallb and traefik resources, along with a service load balancer that has an assigned EXTERNAL-IP.
kubectl get allOutput:
NAME READY STATUS RESTARTS AGE
pod/metallb-1607085578-controller-864c9757f6-bpx6r 1/1 Running 0 81s
pod/metallb-1607085578-speaker-245c2 1/1 Running 0 60s
pod/traefik-1607085579-77bbc57699-b2f2t 1/1 Running 0 81s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 96s
service/traefik-1607085579 LoadBalancer 10.105.119.102 192.168.0.5 80:32153/TCP,443:30791/TCP 84s
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
daemonset.apps/metallb-1607085578-speaker 1 1 1 1 1 kubernetes.io/os=linux 87s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/metallb-1607085578-controller 1/1 1 1 87s
deployment.apps/traefik-1607085579 1/1 1 1 84s
NAME DESIRED CURRENT READY AGE
replicaset.apps/metallb-1607085578-controller-864c9757f6 1 1 1 81s
replicaset.apps/traefik-1607085579-77bbc57699 1 1 1 81sTake note of the EXTERNAL-IP given to the service/traefik-xxx load balancer. In this example, 192.168.0.5 has been assigned and can be used to access services via the Ingress proxy:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/traefik-1607085579 LoadBalancer 10.105.119.102 192.168.0.5 80:32153/TCP,443:30791/TCP 84sReceiving a 404 response here is normal, as you've not configured any Ingress resources to respond yet:
curl http://192.168.0.5404 page not foundDeploy and access the Traefik Dashboard
With an available and addressable load balancer present on your cluster, now you can quickly deploy the Traefik dashboard and access it from anywhere on your LAN (assuming that MetalLB is configured with an addressable range).
Create the Traefik Dashboard IngressRoute in a YAML file:
yamlapiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: name: traefik-dashboard namespace: traefik-system spec: entryPoints: - web - websecure routes: - match: PathPrefix(`/dashboard`) || PathPrefix(`/api`) kind: Rule services: - name: api@internal kind: TraefikServiceDeploy the resource:
shellkubectl apply -f traefik-dashboard.yamlOutput:
shellingressroute.traefik.io/v1alpha1/traefik-dashboard createdAt this point you should be able to access the dashboard using the
EXTERNAL-IPthat you noted above by visitinghttp://192.168.0.5/dashboard/in your browser:Create a simple
whoamiDeployment, Service, and Ingress manifest:yamlapiVersion: apps/v1 kind: Deployment metadata: name: whoami-deployment spec: replicas: 1 selector: matchLabels: app: whoami template: metadata: labels: app: whoami spec: containers: - name: whoami-container image: containous/whoami --- apiVersion: v1 kind: Service metadata: name: whoami-service spec: ports: - name: http targetPort: 80 port: 80 selector: app: whoami --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: whoami-ingress spec: rules: - http: paths: - path: /whoami pathType: Exact backend: service: name: whoami-service port: number: 80Apply the manifests:
shellkubectl apply -f whoami.yamlOutput:
shelldeployment.apps/whoami-deployment created service/whoami-service created ingress.networking.k8s.io/whoami-ingress createdTest the ingress and service:
shellcurl http://192.168.0.5/whoamiOutput:
shellHostname: whoami-deployment-85bfbd48f-7l77c IP: 127.0.0.1 IP: ::1 IP: 10.244.214.198 IP: fe80::b049:f8ff:fe77:3e64 RemoteAddr: 10.244.214.196:34858 GET /whoami HTTP/1.1 Host: 192.168.0.5 User-Agent: curl/7.68.0 Accept: */* Accept-Encoding: gzip X-Forwarded-For: 192.168.0.82 X-Forwarded-Host: 192.168.0.5 X-Forwarded-Port: 80 X-Forwarded-Proto: http X-Forwarded-Server: traefik-1607085579-77bbc57699-b2f2t X-Real-Ip: 192.168.0.82
Further details
With the Traefik Ingress Controller it is possible to use third party tools,
such as ngrok, to go further and expose your load balancer to the world. In
doing this you enable dynamic certificate provisioning through Let's Encrypt,
using either cert-manager or Traefik's own built-in ACME provider.
Internal Upgrading Calico (`docs/internal/upgrading-calico.md`)
Upgrading Calico
K0s bundles Kubernetes manifests for Calico. The manifests are retrieved from
the official Calico repo.
As fetching and modifying the entire multi-thousand line file is error-prone,
you may follow these steps to upgrade Calico to the latest version:
- run
./hack/get-calico.sh <version> - check the git diff to see if it looks sensible
- re-apply our manual adjustments (documented below)
- compile, pray, and test
- commit and create a PR
Manual Adjustments
All manual adjustments should be visible in the git diff. This section provides
a checklist to ensure that these changes are still applied correctly. The code
blocks in this section are k0s-specific modifications. Any such modification
should be accompanied by Go template comments to distinguish them from the
original code.
To see the diff without CRDs, you can do something like:
git diff ':!static/manifests/calico/CustomResourceDefinition'That'll make it easier to spot any needed changes.
Inlining the `calico-config` ConfigMap
Instead of using a ConfigMap to manage the Calico backend and MTU settings,
incorporate them directly into the places where they're used. This will trigger
rolling updates whenever something changes.
This includes the template for the node's CNI plugin configuration (thecni_network_config key in the ConfigMap, the CNI_NETWORK_CONFIG environment
variable of the init container for the calico-node DaemonSet). Calico's CNI
network plugin installer executable ships with a default template, but k0s needs
to tweak this a bit for IPv6 support.
Support for switching Calico backends
Search for .Mode to find:
# Enable IPIP
- name: CALICO_IPV4POOL_IPIP
value: "{{ if eq .Mode "bird" }}{{ .Overlay }}{{ else }}Never{{ end }}"
# Enable or Disable VXLAN on the default IP pool.
- name: CALICO_IPV4POOL_VXLAN
value: "{{ if and (eq .Mode "vxlan") .EnableIPv4 }}{{ .Overlay }}{{ else }}Never{{ end }}"
# Enable or Disable VXLAN on the default IPv6 IP pool.
- name: CALICO_IPV6POOL_VXLAN
value: "{{ if and (eq .Mode "vxlan") .EnableIPv6 }}{{ .Overlay }}{{ else }}Never{{ end }}"
{{- if eq .Mode "vxlan" }}
- name: FELIX_VXLANPORT
value: "{{ .VxlanPort }}"
- name: FELIX_VXLANVNI
value: "{{ .VxlanVNI }}"
{{- end }}iptables auto-detection
# Auto detect the iptables backend
- name: FELIX_IPTABLESBACKEND
value: "auto"Support for enabling WireGuard
{{- if .EnableWireguard }}
- name: FELIX_WIREGUARDENABLED
value: "true"
{{- end }}Support for changing the cluster CIDR
- name: CALICO_IPV4POOL_CIDR
value: "{{ .ClusterCIDR }}"Support for changing the MTU
Search for .MTU to find:
# Set MTU for tunnel device used if ipip is enabled
- name: FELIX_IPINIPMTU
value: "{{ .MTU }}"
# Set MTU for the VXLAN tunnel device.
- name: FELIX_VXLANMTU
value: "{{ .MTU }}"
# Set MTU for the Wireguard tunnel device.
- name: FELIX_WIREGUARDMTU
value: "{{ .MTU }}"Remove bgp support
Remove bgp from CLUSTER_TYPE:
- name: CLUSTER_TYPE
value: "k8s"Disable BIRD checks on liveness and readiness probes when not running the BIRD
backend:
{{- if eq .Mode "bird" }}
- -bird-live
{{- end }}{{- if eq .Mode "bird" }}
- -bird-ready
{{- end }}Remove eBPF support
Comment out the parts of the manifests related to eBPF support. They are usually
commented as such by upstream.
Disable VXLAN offloading
See https://github.com/k0sproject/k0s/issues/3024 for details.
- name: FELIX_FEATUREDETECTOVERRIDE
value: ChecksumOffloadBroken=trueTemplate container image names
Instead of hardcoded image names and versions use placeholders to support
configuration level settings. Following placeholders are used:
CalicoCNIImagefor calico/cniCalicoNodeImagefor calico/nodeCalicoKubeControllersImagefor calico/kube-controllers
Also, all containers in manifests were modified to have the imagePullPolicy
field:
imagePullPolicy: {{ .PullPolicy }}Troubleshooting FAQ (`docs/troubleshooting/FAQ.md`)
Frequently asked questions
How is k0s pronounced?
kay-zero-ess
How do I run a single node cluster?
The cluster can be started with:
k0s controller --singleSee also the Getting Started tutorial.
How do I connect to the cluster?
You find the config in ${DATADIR}/pki/admin.conf (default: /var/lib/k0s/pki/admin.conf). Copy this file, and change the localhost entry to the public IP address of the controller. Use the modified config to connect with kubectl:
export KUBECONFIG=/path/to/admin.conf
kubectl ...Why doesn't `kubectl get nodes` list the k0s controllers?
As a default, the control plane does not run kubelet at all, and will not accept any workloads, so the controller will not show up on the node list in kubectl. If you want your controller to accept workloads and run pods, you do so with:k0s controller --enable-worker (recommended only as test/dev/POC environments).
Is k0s really open source?
Yes, k0s is 100% open source. The source code is licensed under the Apache 2.0
License, and the documentation under a Creative Commons License. The project is
part of the CNCF Sandbox. While Mirantis, Inc. remains a principal contributor
and sponsor, k0s adheres to CNCF's open governance and IP policies to ensure
transparency and community-driven development under a vendor-neutral umbrella.
A kubeconfig created via [`k0s kubeconfig`](../cli/k0s_kubeconfig.md) has been leaked, what can I do?
Kubernetes does not support certificate revocation (see k/k/18982). This means
that you cannot disable the leaked credentials. The only way to effectively
revoke them is to replace the Kubernetes CA for your cluster.
Troubleshooting Certificate Authorities (`docs/troubleshooting/certificate-authorities.md`)
Certificate Authorities (CAs)
Overview of CAs managed by k0s
k0s maintains two Certificate Authorities and one public/private key pair:
- The Kubernetes CA is used to secure the Kubernetes cluster and manage
client and server certificates for API communication. - The etcd CA is used only when managed etcd is enabled, for securing etcd
communications. - The Kubernetes Service Account (SA) key pair is used for signing
Kubernetes service account tokens.
These CAs are automatically created during cluster initialization and have a
default expiration period of 10 years. They are distributed once to all k0s
controllers as part of k0s's join process. Replacing them is a manual process,
as k0s currently lacks automation for CA renewal.
Replacing the Kubernetes CA and SA key pair
The following steps describe a way how to manually replace the Kubernetes CA and
SA key pair by taking a cluster down, regenerating those and redistributing them
to all nodes, and then bringing the cluster back online:
Take a backup! Things might go wrong at any level.
Stop k0s on all worker and controller nodes. All the instructions below
assume that all k0s nodes are using the default data directory/var/lib/k0s. Adjust the path accordingly if a different data directory is
used.Delete the Kubernetes CA and SA key pair files from the all the controller
data directories:/var/lib/k0s/pki/ca.crt/var/lib/k0s/pki/ca.key/var/lib/k0s/pki/sa.pub/var/lib/k0s/pki/sa.key
Delete the kubelet's kubeconfig file and the kubelet's PKI directory from all
worker data directories. Note that this includes controllers that have been
started with the--enable-workerflag:/var/lib/k0s/kubelet.conf/var/lib/k0s/kubelet/pki
Choose one controller as the "first" one. Restart k0s on the first
controller. If this controller is running with the--enable-workerflag,
reboot the machine instead. This ensures that all processes and pods will
be cleanly restarted. After the restart, k0s will have regenerated a new
Kubernetes CA and SA key pair.Distribute the new CA and SA key pair to the other controllers: Copy over the
following files from the first controller to each of the remaining
controllers:/var/lib/k0s/pki/ca.crt/var/lib/k0s/pki/ca.key/var/lib/k0s/pki/sa.pub/var/lib/k0s/pki/sa.key
After copying the files, the new CA and SA key pair are in place. Restart k0s
on the other controllers. For controllers running with the--enable-worker
flag, reboot the machines instead.Rejoin all workers. The easiest way to do this is to use a
kubelet-bootstrap.conffile. You can generate
such a file on a controller like this (see the section on join tokens for
details):shtouch /tmp/rejoin-token && chmod 0600 /tmp/rejoin-token && k0s token create --expiry 1h | base64 -d | gunzip >/tmp/rejoin-tokenCopy that token to each worker node and place it at
/var/lib/k0s/kubelet-bootstrap.conf. Then reboot the machine.When all worker nodes are back online, the
kubelet-bootstrap.conffiles can
be safely removed from the worker nodes. The token can be invalidated without
waiting for its expiration: Usek0s token list --role workerto list all tokens andk0s token invalidate <token-id>to invalidate them
immediately.
See also
Troubleshooting Logs (`docs/troubleshooting/logs.md`)
Logs
k0s runs most of the Kubernetes and other system components as plain Linux child processes. As it acts as a watchdog for the child processes it also combines the logs for all such processes into its own log stream.
k0s adds a "selector" to each components log stream so it is easier to distinguish logs from various components. For example the log stream from kubelet is enhanced with a selector component=kubelet:
Jul 08 08:46:25 worker-kppzr-lls2q k0s[1766]: time="2024-07-08 08:46:25" level=info msg="I0708 08:46:25.876821 1814 operation_generator.go:721] \"MountVolume.SetUp succeeded for volume \\\"kube-api-access-7tfxw\\\" (UniqueName: \\\"kubernetes.io/projected/ca514728-a1de-4408-9be5-8b36ee896752-kube-api-access-7tfxw\\\") pod \\\"node-shell-a16894ee-eb67-4865-8964-44ca5c87e18d\\\" (UID: \\\"ca514728-a1de-4408-9be5-8b36ee896752\\\") \" pod=\"kube-system/node-shell-a16894ee-eb67-4865-8964-44ca5c87e18d\"" component=kubelet stream=stderr
Jul 08 08:46:26 worker-kppzr-lls2q k0s[1766]: time="2024-07-08 08:46:26" level=info msg="I0708 08:46:26.112550 1814 kuberuntime_container_linux.go:167] \"No swap cgroup controller present\" swapBehavior=\"\" pod=\"kube-system/node-shell-a16894ee-eb67-4865-8964-44ca5c87e18d\" containerName=\"shell\"" component=kubelet stream=stderrWhere are the logs?
systemd based setups
systemd stores logs in its journal. Access k0s and all component logs usingjournalctl. For example, to view kubelet logs, run journalctl -u k0sworker | grep component=kubelet.
OpenRC based setups
openRC stores service logs in /var/log/k0sworker.log. To view a specific component log, run grep component=kubelet /var/log/k0s.log.
Troubleshooting Support Dump (`docs/troubleshooting/support-dump.md`)
Support Insight
In many cases, especially when looking for commercial support, there's a need
to share the cluster state with others. While one could always grant access to
the live cluster, this is not always desirable or possible. For these
situations, use the work provided by Troubleshoot.
The Troubleshoot tool can produce a dump of the cluster state for sharing. Thesbctl tool can expose the dump tarball as a Kubernetes API.
The following example shows how this works with k0s.
Setting up
To gather all the needed data, install another tool called support-bundle.
Download it from the Troubleshoot releases page. Make sure to select the
executable for the correct architecture.
Creating support bundle
A Support Bundle needs to know what to collect and optionally, what to analyze. This is defined in a YAML file.
While data collection can be customized to meet specific needs, the following
reference configuration for k0s covers core elements such as:
- collecting info on the host
- collecting system component statuses from
kube-systemnamespace - checking health of Kubernetes API, Etcd etc. components
- collecting k0s logs
- checking status of firewalls, anti-virus etc. services which are known to interfere with Kubernetes
Because host-level information is required, the commands must be run directly on
the k0s nodes.
After setting up the tooling, run the following command to
generate a support bundle:
support-bundle --kubeconfig /var/lib/k0s/pki/admin.conf https://docs.k0sproject.io/stable/support-bundle-<role>.yamlAbove <role> refers to either controller or worker. Different roles
require different information. When running a controller with --enable-worker
or --single, which also makes it a worker, capture a combined dump:
support-bundle --kubeconfig /var/lib/k0s/pki/admin.conf https://docs.k0sproject.io/stable/support-bundle-controller.yaml https://docs.k0sproject.io/stable/support-bundle-worker.yamlOnce the data has been collected, a file namedsupport-bundle-<timestamp>.tar.gz will be created. Share this file as needed.
Troubleshooting Troubleshooting (`docs/troubleshooting/troubleshooting.md`)
Common Pitfalls
There are few common cases we've seen where k0s fails to run properly.
CoreDNS in crash-loop
The most common case we've encountered so far has been CoreDNS getting into crash-loop on the node(s).
With kubectl you see something like this:
$ kubectl get pod --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system calico-kube-controllers-5f6546844f-25px6 1/1 Running 0 167m
kube-system calico-node-fwjx5 1/1 Running 0 164m
kube-system calico-node-t4tx5 1/1 Running 0 164m
kube-system calico-node-whwsg 1/1 Running 0 164m
kube-system coredns-5c98d7d4d8-tfs4q 1/1 Error 17 167m
kube-system konnectivity-agent-9jkfd 1/1 Running 0 164m
kube-system konnectivity-agent-bvhdb 1/1 Running 0 164m
kube-system konnectivity-agent-r6mzj 1/1 Running 0 164m
kube-system kube-proxy-kr2r9 1/1 Running 0 164m
kube-system kube-proxy-tbljr 1/1 Running 0 164m
kube-system kube-proxy-xbw7p 1/1 Running 0 164m
kube-system metrics-server-7d4bcb75dd-pqkrs 1/1 Running 0 167mWhen you check the logs, it'll show something like this:
kubectl -n kube-system logs coredns-5c98d7d4d8-tfs4qplugin/loop: Loop (127.0.0.1:55953 -> :1053) detected for zone ".", see https://coredns.io/plugins/loop#troubleshooting. Query: "HINFO 4547991504243258144.3688648895315093531."CoreDNS forwards queries to the nameservers listed in the resolv.conf file that
the kubelet hands to pods. If that file points to a stub resolver on the host's
loopback interface, the queries come back to CoreDNS, which detects the loop.
K0s handles systemd-resolved's stub automatically: if /etc/resolv.conf points
to it, the kubelet uses /run/systemd/resolve/resolv.conf for pods instead.
Don't disable the stub or empty /etc/resolv.conf to work around the loop. K0s
itself relies on /etc/resolv.conf for name resolution, see the section about
NSS integration. If another resolver on the loopback interface causes the
loop, point the kubelet at a resolv.conf file that lists the upstream
nameservers via the resolvConf field of the kubelet configuration.
Read more in the CoreDNS loop troubleshooting and the Kubernetes DNS
debugging docs.
`k0s controller` fails on ARM boxes
In the logs you probably see etcd not starting up properly.
Etcd is not fully supported on ARM architecture, thus you need
to run k0s controller and thus also etcd process with the environment variableETCD_UNSUPPORTED_ARCH=arm.
As etcd is not fully supported on ARM, it also means that the k0s control plane
with etcd itself is not fully supported on ARM either.
`k0s` will not start on ZFS-based systems
On ZFS-based systems k0s will fail to start because containerd runs by default in overlayfs mode to manage image layers. This is not compatible with ZFS and requires a custom config of containerd. The following steps should get k0s working on ZFS-based systems:
- check with
$ ctr -a /run/k0s/containerd.sock plugins lsthat the containerd ZFS snapshotter plugin is inokstate (should be the case if ZFS kernel modules and ZFS userspace utils are correctly configured):
TYPE ID PLATFORMS STATUS
...
io.containerd.snapshotter.v1 zfs linux/amd64 ok
...- create a containerd config according to the documentation:
$ containerd config default > /etc/k0s/containerd.toml - modify the line in
/etc/k0s/containerd.toml:
...
[plugins.'io.containerd.cri.v1.images']
snapshotter = "overlayfs"
...to
...
[plugins.'io.containerd.cri.v1.images']
snapshotter = "zfs"
...- create a ZFS dataset to be used as snapshot storage at your desired location, e.g.
$ zfs create -o mountpoint=/var/lib/k0s/containerd/io.containerd.snapshotter.v1.zfs rpool/containerd - install k0s as usual, e.g
$ k0s install controller --single -c /etc/k0s/k0s.yaml - containerd should be launched with ZFS support and k0s should initialize the cluster correctly
Pods pending when using cloud providers
Once we enable cloud provider support on kubelet on worker nodes, kubelet will automatically add a taint node.cloudprovider.kubernetes.io/uninitialized for the node. This taint will prevent normal workloads to be scheduled on the node until the cloud provider controller actually runs second initialization on the node and removes the taint. This means that these nodes are not available for scheduling until the cloud provider controller is actually successfully running on the cluster.
For troubleshooting your specific cloud provider see its documentation.
k0s not working with read only `/usr`
By default k0s does not run on nodes where /usr is read only.
This can be fixed by changing the default path for volumePluginDir in your k0s config. You will need to change to values, one for the kubelet itself, and one for Calico.
Here is a snippet of an example config with the default values changed:
spec:
controllerManager:
extraArgs:
flex-volume-plugin-dir: "/etc/kubernetes/kubelet-plugins/volume/exec"
network:
calico:
flexVolumeDriverPath: /etc/k0s/kubelet-plugins/volume/exec/nodeagent~uds
workerProfiles:
- name: coreos
values:
volumePluginDir: /etc/k0s/kubelet-plugins/volume/exec/With this config you can start your controller as usual. Any workers will need to be started with
k0s worker --profile coreos [TOKEN]iptables version mismatch
If the running kernel has firewall rules created using an iptables version
newer than the version shipped with k0s, the Kubernetes network will not
work.
To solve this, make sure that the iptables version used on host is same version
as bundled with k0s. The k0s version is found in /var/lib/k0s/bin.
Profiling
We drop any debug related information and symbols from the compiled binary by utilizing -w -s linker flags.
To keep those symbols use DEBUG environment variable:
DEBUG=true make k0sAny value not equal to the "false" would work.
To add custom linker flags use LDFLAGS variable.
LD_FLAGS="--custom-flag=value" make k0sUsing a custom container runtime and missing labels in Prometheus metrics
With Kubernetes' shift to CRI, Kubelet's method of obtaining container metrics
through its embedded cAdvisor no longer works as it used to. This process
doesn't go via CRI but directly interacts with the container runtime, which is
only natively supported for containerd. K0s automatically manages this for its
built-in containerd runtime. For custom containerd runtimes, you can use the
flag --kubelet-extra-flags=--containerd=/path/to/containerd.sock when starting
k0s.
However, this solution is not applicable to other runtimes such as Docker when
used via the cri-dockerd shim, and there is currently no easy fix. Kubelet is
expected to be refactored in the future to gather container metrics from CRI
instead, as specified in KEP-2371. For now, running a standalone cAdvisor, as
explained in Kubernetes' known issues section, is a
recommended workaround.
Customized configurations
- All data directories reside under
/var/lib/k0s, for example:/var/lib/k0s/kubelet/var/lib/k0s/etcd
Examples Oidc Oidc Cluster Configuration (`docs/examples/oidc/oidc-cluster-configuration.md`)
OpenID Connect integration
Developers use kubectl to access Kubernetes clusters. By default kubectl uses a certificate to authenticate to the Kubernetes API. This means that when multiple developers need to access a cluster, the certificate needs to be shared. Sharing the credentials to access a Kubernetes cluster presents a significant security problem. Compromise of the certificate is very easy and the consequences can be catastrophic.
In this tutorial, we walk through how to set up your Kubernetes cluster to add Single Sign-On support for kubectl using OpenID Connect (OIDC).
OpenID Connect based authentication
OpenID Connect can be enabled by modifying k0s configuration (using extraArgs).
Configuring k0s: overview
There are list of arguments for the kube-api that allows us to manage OIDC based authentication
| Parameter | Description | Example | Required |
|---|---|---|---|
--oidc-issuer-url |
URL of the provider which allows the API server to discover public signing keys. Only URLs which use the https:// scheme are accepted. This is typically the provider's discovery URL without a path, for example "https://accounts.google.com" or "https://login.salesforce.com". This URL should point to the level below .well-known/openid-configuration |
If the discovery URL is https://accounts.google.com/.well-known/openid-configuration, the value should be https://accounts.google.com |
Yes |
--oidc-client-id |
A client id that all tokens must be issued for. | kubernetes | Yes |
--oidc-username-claim |
JWT claim to use as the user name. By default sub, which is expected to be a unique identifier of the end user. Admins can choose other claims, such as email or name, depending on their provider. However, claims other than email will be prefixed with the issuer URL to prevent naming clashes with other plugins. |
sub | No |
--oidc-username-prefix |
Prefix prepended to username claims to prevent clashes with existing names (such as system: users). For example, the value oidc: will create usernames like oidc:jane.doe. If this flag isn't provided and --oidc-username-claim is a value other than email the prefix defaults to ( Issuer URL )# where ( Issuer URL ) is the value of --oidc-issuer-url. The value - can be used to disable all prefixing. |
oidc: |
No |
--oidc-groups-claim |
JWT claim to use as the user's group. If the claim is present it must be an array of strings. | groups | No |
--oidc-groups-prefix |
Prefix prepended to group claims to prevent clashes with existing names (such as system: groups). For example, the value oidc: will create group names like oidc:engineering and oidc:infra. |
oidc: |
No |
--oidc-required-claim |
A key=value pair that describes a required claim in the ID Token. If set, the claim is verified to be present in the ID Token with a matching value. Repeat this flag to specify multiple claims. | claim=value |
No |
--oidc-ca-file |
The path to the certificate for the CA that signed your identity provider's web certificate. Defaults to the host's root CAs. | /etc/kubernetes/ssl/kc-ca.pem |
No |
To set up bare minimum example we need to use:
- oidc-issuer-url
- oidc-client-id
- oidc-username-claim
Configuring k0s: prerequisites
You will require:
- issuer-url
- client-id
- username-claim
Please, refer to providers configuration guide or your selected OIDC provider's own documentation (we don't cover all of them in k0s docs).
Configuration example
apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig
spec:
api:
extraArgs:
oidc-issuer-url: <issuer-url>
oidc-client-id: <client-id>
oidc-username-claim: email # we use email token claim field as a usernameUse the configuration as a starting point. Continue with configuration guide for finishing k0s cluster installation.
OpenID Connect based authorisation
There are two alternative options to implement authorization
Provider based role mapping
Please refer to the providers configuration guide. Generally speaking, using the oidc-groups-claim argument let's you specify which token claim is used a list of RBAC roles for a given user. You still need somehow sync up that data between your OIDC provider and kube-api RBAC system.
Manual roles management
To use manual role management for each user you will need to create a role and role-binding for each new user within k0s cluster.
The role can be shared for all the users.
Role example:
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: default
name: dev-role
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]RoleBinding example:
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: dev-role-binding
subjects:
- kind: User
name: <provider side user id>
roleRef:
kind: Role
name: dev-role
apiGroup: rbac.authorization.k8s.ioThe provided Role example is an all-inclusive and comprehensive example and should be tuned up to your actual requirements.
kubeconfig management
NB: it's not safe to provide full content of the /var/lib/k0s/pki/admin.conf to the end-user. Instead, create a user specific kubeconfig with limited permissions.
The authorization side of the kubeconfig management is described in provider specific guides. Use /var/lib/k0s/pki/admin.conf as a template for cluster specific kubeconfig.
References
OAuth2 spec
Kubernetes authorization system (RBAC)
Kubernetes authenticating system
Examples Oidc Oidc Provider Configuration (`docs/examples/oidc/oidc-provider-configuration.md`)
Providers
We use Google Cloud as a provider for the sake of the example. Check your vendor documentation in case if you use some other vendor.
Notes on stand-alone providers
If you are using stand-alone OIDC provider, you might need to specify oidc-ca-file argument for the kube-api.
Google Cloud
We use k8s-oidc-helper tool to create proper kubeconfig user record.
The issuer URL for the Google cloud is https://accounts.google.com
Creating an application
- Go to the Google Cloud Dashboard
- Create a new project in your organization
- Go to the "Credentials" page
- Create "OAuth consent screen"
Creating a user credentials
- Go to the Google Cloud Dashboard
- Go to the "Credentials" page
- Create new credentials. Select "OAuth client ID" as a type.
- Select "Desktop" app as an application type.
- Save client ID and client secret
Creating kubeconfig user record
Use the command and follow the instructions:
k8s-oidc-helper --client-id=<CLIENT_ID> \
--client-secret=<CLIENT_SECRET> \
--write=trueUsing kubelogin
For other OIDC providers it is possible to use kubelogin plugin.
Please refer to the setup guide for details.
Google Cloud example using `kubelogin`
kubectl oidc-login setup \
--oidc-issuer-url=https://accounts.google.com \
--oidc-client-id=<CLIENT_ID> \
--oidc-client-secret=<CLIENT_SECRET>
kubectl config set-credentials oidc \
--exec-api-version=client.authentication.k8s.io/v1beta1 \
--exec-command=kubectl \
--exec-arg=oidc-login \
--exec-arg=get-token \
--exec-arg=--oidc-issuer-url=https://accounts.google.com \
--exec-arg=--oidc-client-id=<CLIENT_ID> \
--exec-arg=--oidc-client-secret=<CLIENT_SECRET>You can switch the current context to oidc.
kubectl config set-context --current --user=oidc
Governance Cncf Ai Conformance (`docs/governance/cncf/ai-conformance.md`)
Kubernetes AI conformance
This document summarizes how k0s satisfies the Kubernetes AI Conformance requirements for Kubernetes v1.35.
It is a reviewer-facing evidence page built from captured test runs on k0s v1.35.3+k0s.0.
Test environment
The evidence below was captured on a two-node k0s cluster running on Azure:
- k0s v1.35.3+k0s.0
- Kubernetes v1.35.3
- One controller node and one NVIDIA T4 worker node
- GPU Operator v26.3.1 for NVIDIA runtime, device plugin, and DCGM Exporter
DRA support
Requirement: Dynamic Resource Allocation APIs must be available and usable for accelerator allocation.
Status on k0s: Implemented.
k0s v1.35.3+k0s.0 ships Kubernetes v1.35.3, where DRA is available at resource.k8s.io/v1 and enabled by default.
We validated the full DRA flow with the upstream dra-example-driver: a ResourceClaimTemplate was created, the scheduler allocated a simulated GPU device, and the consuming Pod received the allocated device metadata inside the container.
This shows not only that the API surface exists, but that the allocation path is functional end to end on k0s.
Minimal example:
$ kubectl api-resources --api-group=resource.k8s.io
NAME APIVERSION KIND
deviceclasses resource.k8s.io/v1 DeviceClass
resourceclaims resource.k8s.io/v1 ResourceClaim
resourceclaimtemplates resource.k8s.io/v1 ResourceClaimTemplate
resourceslices resource.k8s.io/v1 ResourceSlice
$ kubectl -n dra-demo get resourceclaim
NAME STATE
dra-demo-pod-gpu-nd7zh allocated,reserved
$ kubectl logs -n dra-demo dra-demo-pod | grep GPU_DEVICE_0
GPU_DEVICE_0="gpu-0"Further reading:
Driver runtime management
Requirement: The platform should support installation and management of accelerator drivers and runtime components.
Status on k0s: Implemented.
k0s supports NVIDIA driver and runtime management through the NVIDIA GPU Operator.
On the test cluster, the operator installed the NVIDIA driver, configured the NVIDIA runtime for containerd, and enabled GPU access for workloads that opt into the nvidia RuntimeClass.
This is the standard upstream integration path for NVIDIA-backed Kubernetes clusters and works on k0s with containerd runtime configuration.
Minimal example:
$ kubectl -n gpu-operator exec ds/nvidia-driver-daemonset -- \
nvidia-smi --query-gpu=driver_version --format=csv,noheader
580.126.20
$ kubectl logs cuda-smoketest
NVIDIA-SMI 580.126.20 Driver Version: 580.126.20
$ kubectl get runtimeclass
NAME HANDLER
nvidia nvidia
nvidia-cdi nvidia-cdiFurther reading:
GPU sharing
Requirement: The platform should support mechanisms to share a single physical accelerator among multiple workloads.
Status on k0s: Implemented.
k0s supports GPU sharing through NVIDIA device plugin time-slicing.
On the test cluster, a single Tesla T4 was reconfigured to advertise four schedulable nvidia.com/gpu replicas, and four GPU-requesting Pods ran concurrently on the same physical GPU.
T4 hardware does not support MIG, so time-slicing is the applicable sharing mechanism on this setup.
This demonstrates a real sharing mode on hardware commonly used for inference and smaller training workloads.
Minimal example:
$ kubectl get node k0s-gpu-0 -o jsonpath='{.status.capacity.nvidia\.com/gpu}{"\n"}'
4
$ kubectl get pods -l app=shared-gpu -o wide
NAME READY STATUS NODE
shared-gpu-1 1/1 Running k0s-gpu-0
shared-gpu-2 1/1 Running k0s-gpu-0
shared-gpu-3 1/1 Running k0s-gpu-0
shared-gpu-4 1/1 Running k0s-gpu-0
$ kubectl get node k0s-gpu-0 -o jsonpath='{.metadata.labels.nvidia\.com/gpu\.sharing-strategy}{"\n"}'
time-slicingFurther reading:
Virtualized accelerator
Requirement: The platform should support virtualized accelerators where available.
Status on k0s: Not Implemented.
k0s does not ship a packaged vGPU integration.
Virtualized GPU support depends on NVIDIA vGPU licensing and hardware-specific enablement outside the scope of the k0s distribution.
Users can integrate upstream NVIDIA vGPU components on suitable infrastructure, but that path was not demonstrated in this submission.
Further reading:
AI inference
Requirement: A working Kubernetes Gateway API implementation must support inference traffic management.
Status on k0s: Implemented.
k0s supports Gateway API resources and standard Gateway implementations.
We installed Gateway API v1.5.1 CRDs with Envoy Gateway v1.7.2, then configured a GatewayClass, Gateway, and HTTPRoute to route /predict traffic to a mock inference backend.
Requests matching the configured prefix reached the backend, while non-matching requests were rejected by Envoy.
This provides concrete evidence that k0s can host an inference ingress stack with route attachment and path-based traffic management.
Minimal example:
$ kubectl get gatewayclass eg
NAME CONTROLLER ACCEPTED
eg gateway.envoyproxy.io/gatewayclass-controller True
$ curl -si http://localhost:8888/predict
HTTP/1.1 200 OK
x-app-name: http-echo
{"prediction":0.87}
$ curl -si http://localhost:8888/
HTTP/1.1 404 Not FoundFurther reading:
Gang scheduling
Requirement: The platform must support all-or-nothing scheduling for distributed workloads.
Status on k0s: Implemented.
k0s supports gang scheduling with Volcano.
We installed Volcano v1.14.1 and demonstrated both successful atomic placement and full refusal of an oversized gang: a two-task job was bound together, while a five-task job that could not fit was kept pending without partial placement.
That combination matters because it shows both sides of gang semantics on k0s: atomic success when capacity exists and no partial placement when it does not.
Minimal example:
$ kubectl get vcjob gang-demo
NAME STATUS MINAVAILABLE
gang-demo Completed 2
$ kubectl get vcjob gang-overflow
NAME STATUS MINAVAILABLE
gang-overflow Pending 5
$ kubectl get pods -l volcano.sh/job-name=gang-overflow
No resources found in default namespace.Further reading:
Cluster autoscaling
Requirement: The platform must support scaling accelerator-capable node groups through Kubernetes cluster autoscaling.
Status on k0s: Implemented.
k0s is a Kubernetes distribution and does not bundle its own autoscaler.
Instead, k0s works with the upstream Kubernetes Cluster Autoscaler and the infrastructure-specific provisioning layer behind the cluster, such as Azure VMSS, AWS Auto Scaling Groups, GCP Managed Instance Groups, or Cluster API.
Accelerator-aware scale-up is driven by pending Pods that request nvidia.com/gpu and by the GPU resources advertised through the NVIDIA device plugin.
This is the same model used by other conformant distributions: k0s provides the standard Kubernetes substrate, while autoscaling is supplied by the provisioning layer chosen for the cluster.
Reference configuration:
helm repo add autoscaler https://kubernetes.github.io/autoscaler
helm install cluster-autoscaler autoscaler/cluster-autoscaler \
--namespace kube-system \
--set cloudProvider=azureFurther reading:
Pod autoscaling
Requirement: Horizontal Pod Autoscaler must function correctly for Pods that use accelerators.
Status on k0s: Implemented.
k0s bundles metrics-server, so HPA works out of the box on a default cluster.
We deployed a GPU-requesting Deployment and an autoscaling/v2 HPA targeting CPU utilization.
Under load, HPA increased the Deployment replica count from one to two; the second Pod remained pending because the scheduler correctly enforced single-GPU node capacity.
This is the expected and correct interaction: autoscaling decisions are made from workload metrics, while accelerator capacity is still enforced independently by the scheduler.
Minimal example:
$ kubectl get hpa gpu-burner
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS
gpu-burner Deployment/gpu-burner cpu: 497%/50% 1 2 2
$ kubectl get pods -l app=gpu-burner
NAME READY STATUS
gpu-burner-759c8596bf-7gzvr 1/1 Running
gpu-burner-759c8596bf-k8d4x 0/1 PendingFurther reading:
Accelerator metrics
Requirement: The platform must expose fine-grained accelerator metrics through a standard, machine-readable endpoint.
Status on k0s: Implemented.
k0s supports accelerator metrics through NVIDIA DCGM Exporter, deployed by the GPU Operator.
We integrated DCGM Exporter with kube-prometheus-stack using a ServiceMonitor and verified that Prometheus scraped per-GPU metrics with labels such as UUID, model, PCI bus ID, node name, and driver version.
The important part here is not just that metrics exist, but that they are queryable through the standard Prometheus API with enough labels to identify a specific GPU on a specific node.
Minimal example:
$ curl -s 'http://localhost:9090/api/v1/query?query=DCGM_FI_DEV_GPU_UTIL'
"__name__":"DCGM_FI_DEV_GPU_UTIL"
"UUID":"GPU-df2bfa0a-183e-d378-e4ab-1042a1736a51"
"modelName":"Tesla T4"
"Hostname":"k0s-gpu-0"
"pci_bus_id":"00000001:00:00.0"Further reading:
AI service metrics
Requirement: The platform must discover and collect workload metrics in a standard format.
Status on k0s: Implemented.
k0s supports workload metrics collection through Prometheus Operator compatible monitoring stacks.
We installed kube-prometheus-stack, created a ServiceMonitor for a sample inference-shaped workload, and verified that Prometheus discovered the target and stored its metrics with workload-identifying labels.
This demonstrates the standard collection path most AI services use in practice: workload metrics exposed on /metrics, discovered by ServiceMonitor, and stored in Prometheus.
Minimal example:
$ curl -s 'http://localhost:9090/api/v1/query' \
--data-urlencode 'query=up{job="sample-inference"}'
"job":"sample-inference"
"namespace":"default"
"service":"sample-inference"
"value":[..., "1"]
$ curl -s 'http://localhost:9090/api/v1/query' \
--data-urlencode 'query=node_cpu_seconds_total{job="sample-inference"}'
"__name__":"node_cpu_seconds_total"Further reading:
Secure accelerator access
Requirement: Accelerator access must be isolated so that only authorized workloads can use the device.
Status on k0s: Implemented.
k0s relies on the standard Kubernetes device plugin model for GPU isolation.
On the test cluster, a Pod without a GPU request and without the nvidia RuntimeClass had no access to NVIDIA tooling or device nodes.
When two Pods requested one GPU each on a single-GPU node, the scheduler admitted one and kept the second pending with Insufficient nvidia.com/gpu.
Together, these checks show both isolation-by-default and capacity enforcement for accelerator access on k0s.
Minimal example:
$ kubectl logs no-gpu-request
sh: 1: nvidia-smi: not found
NO_GPU_ACCESS
$ kubectl get pods gpu-hog-1 gpu-hog-2
NAME READY STATUS
gpu-hog-1 1/1 Running
gpu-hog-2 0/1 PendingFurther reading:
Robust controller
Requirement: The platform must be able to run at least one complex AI operator with CRDs and reliable reconciliation behavior.
Status on k0s: Implemented.
k0s supports complex AI operators such as KubeRay.
We installed KubeRay v1.6.1, reconciled a RayCluster into head and worker Pods plus supporting Services, ran a distributed Ray task successfully, and then deleted the head Pod to verify that the operator recreated it and restored the cluster to a ready state.
This goes beyond a basic install check: it shows CRD registration, reconciliation of child resources, a successful workload, and recovery after disruption.
Minimal example:
$ kubectl get raycluster raycluster-demo -o wide
NAME DESIRED WORKERS AVAILABLE WORKERS STATUS
raycluster-demo 1 1 ready
$ kubectl get events --field-selector involvedObject.kind=RayCluster \
--sort-by=.lastTimestamp
CreatedHeadPod
CreatedWorkerPod
DeletedHeadPod
CreatedHeadPod
$ kubectl exec -i "$HEAD_POD" -- python -
Result: [0, 1, 4, 9, 16]
Cluster resources: {'CPU': 2.0, ...}Further reading:
Governance Cncf Gtr (`docs/governance/cncf/gtr.md`)
General Technical Review (GTR)
This document collects information for the CNCF General Technical Review. It is built to support the k0s CNCF Sandbox application but will be a living document that we update regularly.
As we are currently applying for the sandbox level, the document only covers the GTR's “Day 0” phase.
Scope
Roadmap process
The k0s roadmap process is a collaborative effort to align with user needs, community feedback, and emerging trends. We prioritize initiatives that simplify Kubernetes operations and extend its capabilities for far-edge, IoT, and disconnected environments.
The roadmap is a living document, refined iteratively based on technical feasibility, testing, and industry shifts. By emphasizing usabili