k0s Documentation

K0s - The Zero Friction Kubernetes

6,537 stars Go Go 82 files · ~130,833 tokens #kubernetes
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy k0s on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name k0s -p 6443:6443 --restart always docker.io/k0sproject/k0s:{{{ k0s_docker_version }}}
Configured Services & Ports
⚙️ k0s-controller
image: docker.io/k0sproject/k0s:{{{ k0s_docker_version }}}
🔌 Ports: 6443:6443
docker-compose.yml
yaml (docker-compose.yml)
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 ...

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

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:

shell
make

The "package maintainer" way, without building and embedding the required
binaries. This assumes necessary binaries are provided separately at runtime:

shell
make EMBEDDED_BINS_BUILDMODE=none

Docker 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:

shell
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:

shell
make -C embedded-bins

Builds can be done in parallel:

shell
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:

shell
make check-basic

Overview (`docs/README.md`)

k0s - The Zero Friction Kubernetes

{%
include-markdown "../README.md"
start=""
end=""
%}

{%
include-markdown "../README.md"
start=""
end=""
%}

Getting Started

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 results
  • K0S_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:

shell
TestNetworkSuite: bootloosesuite.go:138: bootloose cluster left intact for debugging. Needs to be manually cleaned with: bootloose delete --config /tmp/afghzzvp-bootloose.yaml

This 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

For the local plumbing:

  • A POSIXish environment (env, sh, echo, printf)
  • k0sctl (tested with 0.17.1)
  • jq (tested with ~= 1.6)

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.

shell
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
.

shell
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=ipvs

Apply the configuration:

shell
tofu apply

OpenTofu configuration variables

`os`: Operating system stack

  • al2023: Amazon Linux 2023
  • alpine_3_21: Alpine Linux 3.21
  • alpine_3_24: Alpine Linux 3.24
  • centos_9: CentOS Stream 9
  • centos_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 stream
  • fedora_41: Fedora Linux 41 (Cloud Edition)
  • flatcar: Flatcar Container Linux by Kinvolk
  • oracle_8_9: Oracle Linux Server 8.9
  • oracle_9_3: Oracle Linux Server 9.3
  • rhel_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 SP6
  • ubuntu_2004: Ubuntu 20.04 LTS
  • ubuntu_2204: Ubuntu 22.04 LTS
  • ubuntu_2404: Ubuntu 24.04
  • windows_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_64
  • arm64

`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:

sh
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

  • kuberouter
  • calico

`k0s_kube_proxy_mode`: Mode of operation for kube-proxy

  • iptables
  • ipvs

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 other os_*.tf files 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 do tofu 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:

sh
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:

console
$ 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.yaml

Add -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.

  1. Create the list of images required by k0s.

    shell
    k0s airgap list-images --all >airgap-images.txt
  2. Review this list and edit it according to your needs.

  3. Create the image bundle.

    shell
    k0s 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:

shell
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

  1. Create the list of images required by k0s.

    shell
    k0s airgap list-images --all >airgap-images.txt
  2. Review this list and edit it according to your needs.

  3. Pull the images.

    shell
    xargs -I{} docker pull {} <airgap-images.txt
  4. Create the bundle.

    shell
    docker 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.

console
# mkdir -p /var/lib/k0s/images
# cp image-bundle.tar /var/lib/k0s/images/image-bundle.tar

Via 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:

yaml
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: 0755

Disable 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.

yaml
apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig
metadata:
  name: k0s
spec:
  images:
    default_pull_policy: Never

Autopilot 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.
  • 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)

text
/* Detailed source-code truncated for AI context efficiency. */
  • Lines 7-33 are the two Commands that make up this plan - an airgapupdate and k0supdate.
  • 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.

mermaid
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.

mermaid
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)
MissingSignalNode 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.

text
/* 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 Plan YAML
    • 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
  • Apply the Plan
    • Applying a Plan is a simple kubectl apply operation.
  • Monitor the progress
    • The applied Plan provides a status that details the progress.

Automatic updates

To enable automatic updates, create an UpdateConfig object:

yaml
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 Plan is 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 the Plan, 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 the selector discovery method no longer exist by the time the update
      is ready to be scheduled.

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 /ready will the current controller get sent update signaling.
  • If a previously discovered controller target can no longer be resolved, the Plan transitions
    into an IncompleteTargets state 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 update object payload can provide an optional sha256 hash of the update content
    (specified in url), 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:

yaml
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=worker2

Core Fields

`apiVersion (required)`

  • The current version of the Autopilot API is v1beta2, with a full group-version of autopilot.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 commands contains 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 $GOOS and $GOARCH, separated by a hyphen (-)
      • e.g.: linux-amd64, linux-arm64, linux-arm
    • Note: The main supported platform is linux. Autopilot may work on other platforms, however
      this has not been tested.

`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 (optional)`
  • This object provides the details of how controllers should 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 to 1.
  • 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 (optional)`
  • This object provides the details of how workers should be updated.

`spec.commands[].k0supdate.targets.workers.limits.concurrent (optional, default = 1)`

  • Specifying a concurrent value for worker targets will allow for that number of workers
    to be updated at a time. If no value is provided, 1 is 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 $GOOS and $GOARCH, separated by a hyphen (-)
      • eg: linux-amd64, linux-arm64, linux-arm
    • Note: The main supported platform is linux. Autopilot may work on other platforms, however
      this has not been tested.

`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 (optional)`
  • This object provides the details of how workers should be updated.

`spec.commands[].airgapupdate.workers.limits.concurrent (optional, default = 1)`

  • Specifying a concurrent value for worker targets will allow for that number of workers
    to be updated at a time. If no value is provided, 1 is 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.

yaml
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.

yaml
selector:
    labels: environment=staging
    fields: metadata.name=worker2

Specifying an empty selector will result in all nodes being selected for this target set.

yaml
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.name is available as a query field.

Status Reporting

After a Plan has been applied, its progress can be viewed in the .status of the
autopilot Plan.

shell
kubectl get plan autopilot -oyaml

An example of a Plan status:

yaml
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: SignalPending

To 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 SignalCompleted successfully
    • One is waiting to be signaled (SignalPending)
  • There are also three worker nodes
    • All are awaiting signaling updates (SignalPending)

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 of autopilot.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 updates
  • startTime: At which time of day to check updates
  • length: The length of the update window

`spec.planSpec (optional)`

  • Describes the behavior of the autopilot generated Plan

Example

yaml
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=worker2

FAQ

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>/pki directory)
  • 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:

  • PersistentVolumes of 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:

shell
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:

shell
k0s restore /tmp/k0s_backup_2021-04-26T19_51_57_000Z.tar.gz

The 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 via
spec.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:

shell
gpg --gen-key

The key will be stored in your key ring.

shell
gpg --list-keys

This will output a list of keys:

shell
/home/user/.gnupg/pubring.gpg
------------------------------
pub   4096R/BD33228F 2022-01-13
uid                  Example User <[email protected]>
sub   4096R/2F78C251 2022-01-13

To export the private key for decrypting the backup on another host, note the key ID (BD33228F in this example) in the list and use:

shell
gpg --export-secret-keys --armor BD33228F > k0s.key

To create an encrypted k0s backup:

shell
k0s backup --save-path - | gpg --encrypt --recipient [email protected] > backup.tar.gz.gpg

Restoring 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:

shell
gpg --import k0s.key

To restore the encrypted backup, use:

shell
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:

shell
k0sctl backup

k0sctl 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:

shell
k0sctl apply --restore-from /path/to/backup_file.tar.gz

The 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:

shell
kube-bench run --config-dir docs/kube-bench/cfg/ --benchmark k0s-1.0

Summary of disabled checks

Master Node Security Configuration

The current configuration has in total 8 master checks disabled:

  1. id: 1.2.10 - EventRateLimit requires external yaml config. It is left for the users to configure it

    yaml
    type: skip
    text: "Ensure that the admission control plugin EventRateLimit is set (Manual)"
  2. id: 1.2.12 - By default this isn't passed to the apiserver for air-gap functionality

    yaml
    type: skip
    text: "Ensure that the admission control plugin AlwaysPullImages is set (Manual)"
  3. id: 1.2.22 - For sake of simplicity of k0s all audit configurations are skipped. It is left for the users to configure it

    yaml
    type: skip
    text: "Ensure that the --audit-log-path argument is set (Automated)"
  4. id: 1.2.23 - For sake of simplicity of k0s all audit configuration are skipped. It is left for the users to configure it

    yaml
    type: skip
    text: "Ensure that the --audit-log-maxage argument is set to 30 or as appropriate (Automated)"
  5. id: 1.2.24 - For sake of simplicity of k0s all audit configurations are skipped. It is left for the users to configure it

    yaml
    type: skip
    text: "Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriate (Automated)"
  6. id: 1.2.25 - For sake of simplicity of k0s all audit configurations are skipped. It is left for the users to configure it

    yaml
    type: skip
    text: "Ensure that the --audit-log-maxsize argument is set to 100 or as appropriate (Automated)"
  7. id: 1.2.33 - By default it is not enabled. Left for the users to decide

    yaml
    type: skip
    text: "Ensure that the --encryption-provider-config argument is set as appropriate (Manual)"
  8. id: 1.2.34 - By default it is not enabled. Left for the users to decide

    yaml
    type: skip
    text: "Ensure that encryption providers are appropriately configured (Manual)"

Worker Node Security Configuration

and 4 node checks disabled:

  1. id: 4.1.1 - not applicable since k0s does not use kubelet service file

    yaml
    type: skip
    text: "Ensure that the kubelet service file permissions are set to 644 or more restrictive (Automated)"
  2. id: 4.1.2 - not applicable since k0s does not use kubelet service file

    yaml
    type: skip
    text: "Ensure that the kubelet service file ownership is set to root:root (Automated)"
  3. id: 4.2.6 - k0s does not set this. See https://github.com/kubernetes/kubernetes/issues/66693

    yaml
    type: skip
    text: "Ensure that the --protect-kernel-defaults argument is set to true (Automated)"
  4. id: 4.2.10 - k0s doesn't set this up because certs get auto rotated

    yaml
    type: 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:

  1. id: 3.1.1 - For purpose of being fully automated k0s is skipping this check

    yaml
    type: skip
    text: "Client certificate authentication should not be used for users (Manual)"
  2. 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

    yaml
    type: skip
    text: "Ensure that a minimal audit policy is created (Manual)"
  3. id: 3.2.2 - Same as previous

    yaml
    type: 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.

yaml
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: worker

Deploy 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:

shell
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

text
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:

shell
k0s config validate --config path/to/config/file

config validate sub-command can validate the following:

  1. YAML formatting
  2. SAN addresses
  3. Network providers
  4. 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.

  1. Generate a YAML config file that uses the default settings.

    shell
    mkdir -p /etc/k0s
    k0s config create > /etc/k0s/k0s.yaml
  2. Modify 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.

  3. Install k0s with your new config file.

    shell
    sudo k0s install controller -c /etc/k0s/k0s.yaml
  4. If 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.

    shell
    sudo 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:

text
/* 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.:

yaml
spec:
  network:
    provider: calico
    calico:
      envVars:
        TEST_BOOL_VAR: "true"
        TEST_INT_VAR: "42"
        TEST_STRING_VAR: test

K0s runs Calico with some predefined vars, which can be overwritten by setting new value in spec.network.calico.envVars:

shell
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:

shell
FELIX_IPV6SUPPORT: "false"

In dual-stack mode, there are additional vars:

shell
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 requires
quay.io/k0sproject/kube-proxy:{# no auto-bumps-here #}{{{ "v1.33.2" }}} or
lower.

Default kube-proxy iptables settings:

yaml
iptables:
  masqueradeAll: false
  masqueradeBit: null
  minSyncPeriod: 0s
  syncPeriod: 0s

Default kube-proxy IPVS settings:

yaml
ipvs:
  excludeCIDRs: null
  minSyncPeriod: 0s
  scheduler: ""
  strictARP: false
  syncPeriod: 0s
  tcpFinTimeout: 0s
  tcpTimeout: 0s
  udpTimeout: 0s

Default kube-proxy nftables settings:

yaml
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:

  • clusterDNS
  • clusterDomain
  • apiVersion
  • kind
  • staticPodURL

Configuration examples

Custom volumePluginDir

yaml
spec:
  workerProfiles:
    - name: custom-pluginDir
      values:
        volumePluginDir: /var/libexec/k0s/kubelet-plugins/volume/exec

Eviction Policy

yaml
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

yaml
spec:
  workerProfiles:
    - name: custom-eviction
      values:
        allowedUnsafeSysctls:
          - fs.inotify.max_user_instances

`spec.featureGates`

Available components are:

  • kube-apiserver
  • kube-controller-manager
  • kubelet
  • kube-scheduler
  • kube-proxy

If components is omitted, propagates to all Kubernetes components.

Modifies extraArgs.

Examples

Generic feature gates example

yaml
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: false

Kubelet feature gates example

The below is an example of a k0s config with feature gates enabled:

yaml
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:

yaml
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.konnectivity
  • spec.images.metricsserver
  • spec.images.kubeproxy
  • spec.images.coredns
  • spec.images.pause
  • spec.images.calico.cni
  • spec.images.calico.node
  • spec.images.calico.kubecontrollers
  • spec.images.kuberouter.cni
  • spec.images.kuberouter.cniInstaller
  • spec.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') -%}

yaml
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 as
airgap-repo.local/k0sproject/calico-kube-controllers:{{{ cali_ver }}} and
airgap-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.

  • agentPort agent port to listen on (default 8132)
  • adminPort admin 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.

yaml
spec:
  telemetry:
    enabled: true

Component 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

yaml
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: 256Mi

Disabling 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:

text
--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.:

yaml
spec:
  hosts:
    - role: controller
      installFlags:
        - --disable-components=metrics-server

As 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:

shell
sudo k0s install worker --token-file /var/lib/k0s/join-token --kubelet-root-dir=/var/lib/kubelet

Keep 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:

  1. 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.
  2. 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.
  3. A virtual router ID, which defaults to 51. This virtual router ID must be unique in the broadcast domain.
  4. 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:

yaml
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:

yaml
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 and
unicastPeers 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:

yaml
spec:
  network:
    controlPlaneLoadBalancing:
      enabled: true
      type: Keepalived
      keepalived:
        vrrpInstances:
        - virtualIPs: ["<VIP address>/<netmask>"] # for instance ["2001:db8:2::1/64"]
          authPass: "<my password>"
          addressLabel: 30000

The label value can be verified using iproute2:

shell
$ ip addrlabel | grep 2001:db8:2::1
prefix 2001:db8:2::1/128 label 30000

The 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:

  1. A userspace reverse proxy running in the k0s process. This is the default and recommended setting.
  2. 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.

yaml
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:

  1. It's incompatible with controller+worker.
  2. May not work on every infrastructure.
  3. Troubleshooting is significantly more complex.
  4. 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.
yaml
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.100

Full example using `k0sctl`

The following example shows a full k0sctl configuration file featuring three
controllers and three workers with control plane load balancing enabled.

text
/* 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:

text
/* 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:

console
k0sctl kubeconfig > k0s-kubeconfig
export KUBECONFIG=$(pwd)/k0s-kubeconfig

All three worker nodes are ready:

console
$ 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 }}}+k0s

Only one controller has the VIP:

console
$ 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 2399sec

The 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:

console
$ 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:

console
$ 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 2182sec

And the cluster will be working normally:

console
$ 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 }}}+k0s

Troubleshooting

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:

console
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 forever
console
controller1:/# 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 forever

If 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.

console
controller0:/# ip a s dummyvip0 | grep 172.17.0.102
    inet 172.17.0.102/32 scope global dummyvip0
console
controller1:/# ip a s dummyvip0 | grep 172.17.0.102
    inet 172.17.0.102/32 scope global dummyvip0

If this isn't present in the nodes, keepalived logs can be seen in the k0s-logs, and
can be filtered with component=keepalived.

console
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 the
defaultnamespace:

console
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   9m14s

You can see the cplb-reconciler updates by running:

console
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-reconciler

Troubleshooting 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:

console
controller0:/# netstat -tlpn | grep 6444
tcp        0      0 :::6444                 :::*                    LISTEN      345/k0s

Then the requests to the VIP on the apiserver port are forwarded to this socket using one iptables rule:

console
-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:

console
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 6444

Keep 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:

console
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:8B

Note: 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 to keepalived-virtualservers-generated.conf
    if the Keepalived VRRP instance's current state is master or to /dev/null if it's backup. This file is only generated if
    there is exactly one VRRP instance.

Additionally, you can check the actual IPVS configuration using ipvsadm:

console
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:

yaml
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.conf

It is recommended to create custom templates based on the default templates, these can be accessed using the following commands:

shell
# View the default VRRP template
k0s keepalived-config vrrp

# View the default Virtual Servers template
k0s keepalived-config virtualservers

The 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:

shell
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.

shell
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.

shell
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/k0s on Linux, C:\var\lib\k0s on Windows)
    persistent state
  • --run-dir (default /run/k0s on 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-dir flags
    passed to k0s and 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>/kubelet rather than /var/lib/kubelet).
  • Backup/restore covers pki/, manifests/, and images/, 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:

yaml
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::/108

This 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:

yaml
spec:
  controllerManager:
    extraArgs:
      node-cidr-mask-size-ipv6: "120"
      node-cidr-mask-size-ipv4: "21"
  network:
    dualStack:
      enabled: true
      IPv6podCIDR: fd00::/108
      IPv6serviceCIDR: fd01::/108

Using 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:

yaml
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::/108

Specifying 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 explicitly
spec.api.externalAddress or spec.api.address.

To explicitly define the family which will be used by default use the following
configuration:

yaml
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:


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 setup
  • spec.storage - these options configure how the local storage (etcd or SQLite) is setup
  • spec.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:

shell
k0s config edit

This 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.:

yaml
kuberouter:
      mtu: 1350
      autoMTU: false

This 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.podCIDR
  • network.serviceCIDR
  • network.provider
  • network.controlPlaneLoadBalancing
  • network.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:

shell
k0s config status
shell
LAST 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 calico

Environment 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, so
ETCD_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:

shell
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
EOT

OpenRC

Export the desired environment variable overriding service configuration in the
/etc/conf.d directory:

shell
echo 'export HTTP_PROXY="192.168.33.10:3128"' > /etc/conf.d/k0scontroller

Experimental 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:

shell
k0s token create --role=worker > worker-token.txt

Transfer 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:

powershell
Install-WindowsFeature -Name Containers

Note: A system reboot is required after enabling this feature.

3. Download and install k0s

After rebooting, open PowerShell as Administrator and run:

powershell
# 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 start

Note: 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

shell
kubectl run win --image=hello-world:nanoserver --command=true -i --attach=true -- cmd.exe

Manifest for pod with IIS web-server

yaml
apiVersion: v1
kind: Pod
metadata:
  name: iis
spec:
  containers:
  - name: iis
    image: mcr.microsoft.com/windows/servercore/iis
    imagePullPolicy: IfNotPresent

Extensions (`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

text
/* 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/prometheus chart of the specified version to the default namespace.

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

yaml
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):

shell
k0s sysinfo

Linux 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.

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 enable
CONFIG_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:

yaml
...
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:

text
> 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:

  • cpu
  • cpuacct
  • cpuset
  • memory
  • devices
  • freezer
  • pids

Optional cgroup controllers:

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:

text
127.0.0.1 localhost
::1 localhost

The 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:

text
fs.inotify.max_user_instances = 8192
  1. Check the current value:

    bash
    cat /proc/sys/fs/inotify/max_user_instances
  2. To apply the new value persistently, create a sysctl configuration file,
    for example, /etc/sysctl.d/99-inotify.conf, and place the parameter
    setting from above in it.

  3. Load the new kernel values:

    bash
    sudo 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:

shell
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

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:

yaml
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-charts

Apply 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

yaml
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-nginx

With Authentication

yaml
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: mytoken

OCI Registry (Public)

yaml
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 registries

OCI Registry with Authentication

yaml
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-token

OCI Registry with Custom CA and mTLS

yaml
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.key

Local Chart File

yaml
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 files

Secret-Based Authentication

For sensitive credentials, use Kubernetes Secrets instead of embedding them in Chart resources:

bash
# Create a secret with repository credentials
kubectl create secret generic helm-registry-creds \
  --from-literal=username=myuser \
  --from-literal=password=mypassword \
  -n kube-system
yaml
apiVersion: 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 omitted

Secret Keys:

  • url - Repository URL
  • username - HTTP Basic auth username
  • password - HTTP Basic auth password
  • ca.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:

yaml
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 fields

Chart 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:

bash
kubectl get charts -n kube-system
kubectl describe chart prometheus -n kube-system

Note: 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.yaml and 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 the upgrade command)
  • --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:

yaml
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: default

Add 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:

  1. For each chart in your spec.extensions.helm.charts section:

    • 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
  2. Optionally remove charts from k0s.yaml (both methods can coexist)

Example migration:

Before (k0s.yaml):

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: default

After (Chart resource):

yaml
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-charts

Example extensions that work well with k0s:

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:

txt
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:

txt
http://<ip-addr>:9000

Restart HAProxy to apply the configuration changes.

k0s configuration

First and foremost, all controllers should utilize the same CA certificates and SA key pair:

txt
/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.crt

To 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.

yaml
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.

yaml
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

  1. Download k0s

    Run the k0s download script to download the latest stable version of k0s and make it executable from /usr/local/bin/k0s.

    shell
    curl --proto '=https' --tlsv1.2 -sSf https://get.k0s.sh | sudo sh

    Alternatively, download it from the k0s releases page. This approach is
    required in air gapped environments.

  2. Install k0s as a service

    The k0s install sub-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:

    shell
    sudo k0s install controller --single

    Note: The --single option 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:

    shell
    sudo k0s install controller --enable-worker --no-taints

    The k0s install controller sub-command accepts the same flags and parameters as the k0s controller. Refer to manual install for a custom config file example.

    It is possible to set environment variables with the install command:

    shell
    sudo k0s install controller -e ETCD_UNSUPPORTED_ARCH=arm

    The system service can be reinstalled with the --force flag:

    shell
    sudo k0s install controller --single --force
    sudo systemctl daemon-reload

    If you need to reinstall and immediately start the service, combine --force with --start:

    shell
    sudo k0s install controller --single --force --start

    When used with --start, the --force flag will also restart an already running service.

  3. Start k0s as a service

    To start the k0s service, run:

    shell
    sudo k0s start

    Alternatively, both steps can be combined by using the --start flag with the install command:

    shell
    sudo k0s install controller --single --start

    The k0s service will start automatically after the node restart.

    A minute or two typically passes before the node is ready to deploy applications.

  4. 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-systemd
  5. Access 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.

  1. Stop the service.

    shell
    sudo k0s stop
  2. Execute the k0s reset command.

    The k0s reset command cleans up the installed system service, data directories, containers, mounts and network namespaces.

    shell
    sudo k0s reset
  3. Reboot the system.

    A few small k0s fragments persist even after the reset, such as iptables
    rules. Reboot the machine after running k0s reset.

Next Steps


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":

sh
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:

  • -d runs the container in detached mode, i.e. in the background.
  • --name k0s-controller names the container "k0s-controller".
  • --hostname k0s-controller sets the host name of the container to
    "k0s-controller".
  • -v /var/lib/k0s -v /var/log/pods creates two Docker volumes and mounts them
    to /var/lib/k0s and /var/log/pods respectively inside the container,
    ensuring that cluster data persists across container restarts.
  • --tmpfs /run TODO
  • --privileged gives 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:6443 exposes 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:

dockerfile
{% include "../Dockerfile" start="# Start CMD" end="# End CMD" %}

Alternatively, a controller-only node can be run like this:

sh
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 controller

Note 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.

  1. Acquire a join token for the
    worker:

    sh
    token=$(docker exec k0s-controller k0s token create --role=worker)
  2. Run the container to create and join the new worker:

    sh
    docker 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 $token

    Alternatively, with fine-grained privileges:

    sh
    docker 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=unconfined is required for runc to access the
      session keyring.
    • --device /dev/kmsg makes /dev/kmsg visible from inside the container.
      The kubelet's OOM watcher uses this.

    Notes on Linux capabilities:

    • CAP_SYS_ADMIN allows for a variety of administrative tasks, including
      mounting file systems and managing namespaces, which are necessary for
      creating and configuring nested containers.

    • CAP_NET_ADMIN allows manipulation of network settings such as interfaces
      and routes, allowing containers to create isolated or bridged networks, and
      so on.

    • CAP_SYS_PTRACE allows to inspect and modify processes, used to monitor
      other containers in a nested environment.

    • CAP_SYS_RESOURCE allows containers to override resource limits for things
      like memory or file descriptors, used to manage and adjust resource
      allocation in nested container environments.

    • CAP_SYSLOG allows 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:

sh
docker exec k0s-controller k0s kubectl get nodes

b) Using kubectl locally

To configure local access to your k0s cluster, follow these steps:

  1. Generate the kubeconfig:

    sh
    docker exec k0s-controller k0s kubeconfig admin > ~/.kube/k0s.config
  2. Update kubeconfig with Localhost Access:

    To automatically replace the server IP with localhost dynamically in ~/.kube/k0s.config, use the following command:

    sh
    sed -i '' -e "$(awk '/server:/ {print NR; exit}' ~/.kube/k0s.config)s|https://.*:6443|https://localhost:6443|" ~/.kube/k0s.config

    This command updates the kubeconfig to point to localhost, allowing access to the API server from your host machine

  3. Set the KUBECONFIG Environment Variable:

    sh
    export KUBECONFIG=~/.kube/k0s.config
  4. Verify Cluster Access:

    sh
    kubectl 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:

yaml
{% include "compose.yaml" %}

Below is a more complex example, using Traefik as a load balancer, along with
three controller and three worker nodes:

yaml
{% include "compose-cluster.yaml" %}

Running the above:

console
❯ 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.5s

After a short while:

console
$ 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          1m37s

Known 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


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.

shell
curl --proto '=https' --tlsv1.2 -sSf https://get.k0s.sh | sudo sh

The 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:

shell
curl --proto '=https' --tlsv1.2 -sSf https://get.k0s.sh | sudo K0S_VERSION={{{ k0s_version }}} sh

2. Bootstrap a controller node

Create a configuration file:

shell
mkdir -p /etc/k0s
k0s config create > /etc/k0s/k0s.yaml

Note: For information on settings modification, refer to the configuration documentation.

shell
sudo k0s install controller -c /etc/k0s/k0s.yaml
shell
sudo k0s start

Alternatively, both steps can be combined with the --start flag:

shell
sudo k0s install controller -c /etc/k0s/k0s.yaml --start

The system service can be reinstalled with the --force flag. When combined with --start, it will also restart an already running service:

shell
sudo k0s install controller -c /etc/k0s/k0s.yaml --force --start

k0s 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:

shell
sudo k0s token create --role=worker

The 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:

shell
sudo k0s token create --role=worker --expiry=100h > token-file

4. Add workers to the cluster

To join the worker, run k0s in the worker mode with the join token you created:

shell
sudo k0s install worker --token-file /path/to/token/file
shell
sudo k0s start

Alternatively, both steps can be combined with the --start flag:

shell
sudo k0s install worker --token-file /path/to/token/file --start

The system service can be reinstalled with the --force flag. When combined with --start, it will also restart an already running service:

shell
sudo k0s install worker --token-file /path/to/token/file --force --start

About 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:

shell
sudo k0s token create --role=controller --expiry=1h > token-file

On the new controller, run:

shell
sudo k0s install controller --token-file /path/to/token/file -c /etc/k0s/k0s.yaml

Important 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.

shell
sudo k0s start

Alternatively, both steps can be combined with the --start flag:

shell
sudo k0s install controller --token-file /path/to/token/file -c /etc/k0s/k0s.yaml --start

The system service can be reinstalled with the --force flag. When combined with --start, it will also restart an already running service:

shell
sudo k0s install controller --token-file /path/to/token/file -c /etc/k0s/k0s.yaml --force --start

6. Check k0s status

Run the following command to check the k0s instance status:

shell
sudo k0s status
shell
Version: {{{ k0s_version }}}
Process ID: 2769
Parent Process ID: 1
Role: controller
Init System: linux-systemd
Service file: /etc/systemd/system/k0scontroller.service

7. Access your cluster

Use the Kubernetes 'kubectl' command-line tool that comes with k0s binary to deploy your application or check your node status:

shell
sudo k0s kubectl get nodes
shell
NAME   STATUS   ROLES    AGE    VERSION
k0s    Ready    <none>   4m6s   {{{ k8s_version }}}+k0s

You can also access your cluster easily with Lens, simply by copying the kubeconfig and pasting it to Lens:

shell
sudo cat /var/lib/k0s/pki/admin.conf

Note: 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


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

  1. Run the following command to create a k0sctl configuration file:

    shell
    k0sctl init > k0sctl.yaml

    This action creates a k0sctl.yaml file in the current directory:

    yaml
    apiVersion: 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_rsa
  2. Provide 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:

text
/* 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.

shell
k0sctl kubeconfig > kubeconfig

With the kubeconfig, you can access your cluster using either kubectl or Lens.

shell
kubectl get pods --kubeconfig kubeconfig -A
shell
NAMESPACE     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          3m51s

Known 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


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/manifests is 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:

yaml
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: 80

New pods will appear soon thereafter.

shell
sudo k0s kubectl get pods --namespace nginx
shell
NAME                                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          10m

Networking (`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:

xml
{% 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:

xml
{% 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:

console
$ sudo firewall-cmd --permanent --add-service=k0s-controller
success

For nodes running worker components:

console
$ sudo firewall-cmd --permanent --add-service=k0s-worker
success
$ sudo firewall-cmd --permanent --add-masquerade
success

For 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:

console
$ sudo firewall-cmd --permanent --add-source=<podCIDR>
success
$ sudo firewall-cmd --permanent --add-source=<serviceCIDR>
success

Replace `` and <serviceCIDR> with the values configured in
k0s.yaml. When you schedule workloads on controller nodes, add both
k0s-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:

console
$ sudo firewall-cmd --reload
success

For automated image builds where firewalld is not yet running (for example
during cloud-init), use the offline variant:

console
$ 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 firewalld

After reloading, you should see the k0s services applied:

console
$ 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 or
kubectl), 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-empty
    spec.api.externalAddress.
  • K0s isn't running as a single node, i.e. it isn't
    started using the --single flag.
  • 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):

yaml
spec:
  network:
    nodeLocalLoadBalancing:
      enabled: true
      type: EnvoyProxy

Or alternatively, if using k0sctl, add the following to
the k0sctl configuration (k0sctl.yaml):

yaml
spec:
  k0s:
    config:
      spec:
        network:
          nodeLocalLoadBalancing:
            enabled: true
            type: EnvoyProxy

All 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:

yaml
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: k0s

Save the above configuration into a file called k0sctl.yaml and apply it in
order to bootstrap the cluster:

text
/* 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:

shell
k0sctl kubeconfig > k0s-kubeconfig
export KUBECONFIG=$(pwd)/k0s-kubeconfig

The three controllers are available and provide API Server endpoints:

console
$ 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   2m49s

The 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 -%}

console
$ 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:

console
$ 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:

console
$ 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:

console
$ kubectl get nodes
Unable to connect to the server: dial tcp 10.81.146.254:6443: connect: no route to host

Changing 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.

console
$ 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 the
default/kubernetes Endpoints resource and its k0s controller lease is orphaned:

console
$ 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   4m47s

Despite that controller being unavailable, the cluster remains operational. The
third controller has become the new k0s leader. Workloads will run just fine:

console
$ 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 28

Podsecurity (`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:

text
```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:

text
```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:

console
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=progress

Note: 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:

console
growpart /dev/mmcblk0 2
resize2fs /dev/mmcblk0p2

Ubuntu 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:

shell
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.

  1. To create a swap file:

    shell
    fallocate -l 2G /swapfile && \
    chmod 0600 /swapfile && \
    mkswap /swapfile && \
    swapon -a
  2. Ensure 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/*.

  3. Ensure that your swap is mounted after reboots by confirming that the
    following line exists in your /etc/fstab configuration:

    shell
    /swapfile         none           swap sw       0 0

Download k0s

Download a k0s release. For
example:

shell
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.

shell
curl --proto '=https' --tlsv1.2 -sSf https://get.k0s.sh | sudo sh

At this point you can run k0s:

console
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:

text
/* 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:

console
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.io

Start it:

text
/* 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:

console
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:

console
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          0B

As 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:

console
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.io

Start it:

text
/* 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:

console
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:

console
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          0B

This 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:

text
/* 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):

console
sudo sh -c 'mkdir -p /var/lib/k0s/ && umask 077 && echo "$TOKEN_CONTENT" > /var/lib/k0s/join-token'

After that, install the k0sworker service:

console
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.io

Start the service:

text
/* Detailed source-code truncated for AI context efficiency. */

As this is a worker node, we cannot access the Kubernetes API via the builtin
k0s kc subcommand, but we can check the k0s API instead:

console
ubuntu@ubuntu:~$ sudo k0s status
Version: {{{ k0s_version }}}
Process ID: 1631
Role: worker
Workloads: true
SingleNode: false

The memory requirements are also pretty low:

console
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          0B

Connect to the cluster

On a controller node, generate a new raspi-cluster-master user with admin
rights and get a kubeconfig for it:

text
/* Detailed source-code truncated for AI context efficiency. */

Using the above kubeconfig, you can now access and use the cluster:

text
/* 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

  1. Typically, you can enable SSH while creating the bootable drive for Raspberry Pi OS. If not, you can enable it later

  2. 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:

    bash
    ssh-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.pub is the default location for the public ssh key. If your key is elsewhere, adjust accordingly.

  3. Verify SSH access

    shell
    ssh <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:

  1. Edit /boot/cmdline.txt:

    bash
    sudo nano /boot/cmdline.txt

    Append (on the same single line):

    bash
    cgroup_enable=memory cgroup_memory=1
  2. Reboot

    bash
    sudo reboot

Install k0s

Download k0s

Download a k0s release. For
example:

shell
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.

shell
curl --proto '=https' --tlsv1.2 -sSf https://get.k0s.sh | sudo sh

At this point you can run k0s:

console
$ 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:

text
/* 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:

yaml
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+worker

SSH 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:

  1. Start the SSH agent (if not already running)

    bash
    eval "$(ssh-agent -s)"
  2. Add your private key (you’ll be prompted for the passphrase)

    bash
    ssh-add ~/.ssh/id_rsa
  3. Verify the key is loaded

    bash
    ssh-add -l

Deploy cluster

  1. Apply the cluster.yaml using k0sctl on your local machine.

    text

/* Detailed source-code truncated for AI context efficiency. */

text
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:

shell
kubectl drain node.hostname

Access your node (e.g. via ssh) to stop and reset k0s:

shell
sudo k0s stop
sudo k0s reset

Reboot the node (for good measure):

shell
sudo systemctl reboot

Once the node is available again, run k0sctl apply to integrate it into your cluster and uncordon the node to allow pods to be scheduled:

shell
k0sctl apply -c config.yaml
kubectl uncordon node.hostname

Releases (`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:

text
{{{ 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:

shell
# 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:

console
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:

shell
# 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:

shell
k0s stop
k0s reset
reboot

Declarative 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.

shell
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    True

By 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:

console
$ kubectl patch etcdmember controller2 -p '{"spec":{"leave":true}}' --type merge
etcdmember.etcd.k0sproject.io/controller2 patched

The join/leave status is tracked in the object's conditions. This allows you to
wait for the leave to actually happen:

console
$ kubectl wait etcdmember controller2 --for condition=Joined=False
etcdmember.etcd.k0sproject.io/controller2 condition met

You'll see the node left etcd cluster:

console
$ 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
console
$ 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.
  • 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:

  1. Stop the service:

    console
    sudo k0s stop
  2. Invoke the reset command:

    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.

  1. Invoke k0sctl reset command:

    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:

toml
# k0s_managed=true

If 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:

shell
containerd config default > /etc/k0s/containerd.toml

k0s runs containerd with the following default values:

shell
/var/lib/k0s/bin/containerd \
    --root=/var/lib/k0s/containerd \
    --state=/run/k0s/containerd \
    --address=/run/k0s/containerd.sock \
    --config=/etc/k0s/containerd.toml

Next, add the following default values to the configuration file:

toml
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 sets
version = 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 as
docker-mirror.internal.acme.corp. Then you could use the following drop-in
configuration:

/etc/k0s/containerd.d/cri-registry.toml:

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:

toml
# 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.

  1. 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.

  2. Prepare the config for k0s managed containerd, to utilize gVisor as additional runtime:

    shell
    cat <<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"
    EOF
  3. Start and join the worker into the cluster, as normal:

    shell
    k0s worker $token
  4. Register containerd to the Kubernetes side to make gVisor runtime usable for workloads (by default, containerd uses normal runc as the runtime):

    shell
    cat <<EOF | kubectl apply -f -
    apiVersion: node.k8s.io/v1
    kind: RuntimeClass
    metadata:
      name: gvisor
    handler: runsc
    EOF

    At this point, you can use gVisor runtime for your workloads:

    yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: nginx-gvisor
    spec:
      runtimeClassName: gvisor
      containers:
      - name: nginx
        image: nginx
  5. (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:

shell
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-operator

With 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:

  1. Manually install required components.
    On each k0s worker and k0s controller --enable-worker node, both
    Docker Engine and cri-dockerd need to be installed manually. Follow the
    official Docker Engine installation guide and cri-dockerd
    installation instructions
    .

  2. Configure and restart affected k0s nodes.
    Once installations are complete, the nodes needs to be restarted with the
    --cri-socket flag 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

console
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:

console
sudo k0s install --force worker --cri-socket=remote:unix:///var/run/cri-dockerd.sock

or, respectively

console
sudo k0s install --force controller --enable-worker --cri-socket=remote:unix:///var/run/cri-dockerd.sock

In scenarios where Docker is managed via systemd, it is crucial that the
cgroupDriver: 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:

yaml
apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig
metadata:
  name: k0s
spec:
  workerProfiles:
    - name: systemd-docker-cri
      values:
        cgroupDriver: systemd

Note 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 -%}

console
$ 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.7

On the worker nodes, the Kubernetes containers should be listed as regular
Docker containers:

text
/* 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.

shell
$ getenforce
Enforcing

Install 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:

shell
yum install -y container-selinux

In the rest of the metnioned distributions run:

shell
dnf install -y container-selinux

Set SELinux labels for k0s installation files

Run below commands on the host OS of the worker nodes.

shell
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/cni

Enable 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.

toml
[plugins.'io.containerd.cri.v1.runtime']
  enable_selinux = true

Verify SELinux works in Kubernetes environment

By following the example Assign SELinux labels to a Container, deploy a testing pod using below YAML file:

yaml
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:

shell
$ 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 infinity

Node-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:

shell
$ 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.yaml

Shell 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:

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:

bash
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
EOF

Then 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:

zsh
echo "autoload -Uz compinit; compinit" >>~/.zshrc

Place the completion script in a custom site-functions folder:

zsh
mkdir -p -- ~/.local/share/zsh/site-functions
k0s completion zsh >~/.local/share/zsh/site-functions/_k0s

Edit ~/.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:

zsh
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 k0s

Then 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:

shell
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:

powershell
k0s completion powershell > C:\path\to\k0s.ps1

You can import it like so:

powershell
Import-Module C:\path\to\k0s.ps1

To 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 IPv6
podCIDR 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.

yaml
spec:
  network:
    podCIDR: fd00::/108
    serviceCIDR: fd01::/108

Ensure that every controller in the cluster is executed with the
IPv6SingleStack feature gate enabled:

shell
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:

yaml
spec:
  controllerManager:
    extraArgs:
      node-cidr-mask-size: "120"
  network:
    podCIDR: fd00::/108
    serviceCIDR: fd01::/108

With 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"]

text
# 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

text
- 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:

shell
sudo k0s install controller --enable-metrics-scraper

Once enabled, a new set of objects will appear in the cluster:

shell
❯ ~ 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       43h

That's not enough to start scraping these additional metrics. For Prometheus
Operator
based solutions, you can create a
ServiceMonitor for it like this:

yaml
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: metrics

Note that it won't clear alerts like KubeControllerManagerDown or
KubeSchedulerDown 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:

promql
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_64
  • aarch64
  • armv7l
  • riscv64 (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, use
k0s 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.

shell
sudo k0s stop

Now 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.

shell
curl --proto '=https' --tlsv1.2 -sSf https://get.k0s.sh | sudo sh

Then you can start the service (with the upgraded k0s) and your upgrade is done.

shell
sudo k0s start

Upgrade 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:

  1. Upgrade of each controller, one at a time. There is no downtime if multiple controllers are configured.

  2. Upgrade of workers, in batches of 10%.

  3. 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-drain option.)

  4. 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:

yaml
spec:
  k0s:
    version: {{{ k0s_version }}}

If you do not specify a version, k0sctl checks online for the latest version and defaults to it.

shell
k0sctl apply
shell
...
...
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 kubeconfig

User 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.

shell
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:

shell
k0s kubeconfig create --groups "system:masters" testUser > k0s.config

Create a roleBinding to grant the user access to the resources:

shell
k0s kubectl create clusterrolebinding --kubeconfig k0s.config testUser-admin-binding --clusterrole=admin --user=testUser

Verifying 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 -%}

shell
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 }}}-amd64

Version 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 k0s
v1.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:

  1. Upgrade controllers first, one minor at a time (e.g. v1.30 → v1.31,
    not v1.30 → v1.32).
  2. Upgrade workers after the control plane is fully on the new minor.
  3. 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 -%}

console
$ 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=linux

Controller worker nodes are assigned node.k0sproject.io/role=control-plane and node-role.kubernetes.io/control-plane=true labels:

console
$ 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=true

Note: 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.

shell
kubectl get nodes -o custom-columns=NAME:.metadata.name,TAINTS:.spec.taints
shell
NAME          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 enabling
serverTLSBootstrap 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 serving
kubectl 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 the system:node:<nodeName> name and be a member of
    the system:nodes group, 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 be kubernetes, kubernetes.default, or a name
    within the svc or cluster domains.
  • The node exists in the cluster.
  • The node's status.addresses contains 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).

shell
k0s worker --iptables-mode=nft

Architecture 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:

go
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:

go
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 by
k8s.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

yaml
url: oci://my.registry/binaries/k0s:v1.30.1+k0s.0
sha256: e95603f167cce6e3cffef5594ef06785b3c1c00d3e27d8e4fc33824fe6c38a99
secretRef:
  namespace: kube-system
  name: artifacts-registry

Configuration for OCI using plain HTTP transport

yaml
url: oci+http://my.registry/binaries/k0s:v1.30.1+k0s.0
sha256: e95603f167cce6e3cffef5594ef06785b3c1c00d3e27d8e4fc33824fe6c38a99
secretRef:
  namespace: kube-system
  name: artifacts-registry

Configuration for HTTPS

yaml
url: https://my.file.server/binaries/k0s-v1.30.1+k0s.0
sha256: e95603f167cce6e3cffef5594ef06785b3c1c00d3e27d8e4fc33824fe6c38a99
secretRef:
  namespace: kube-system
  name: artifacts-basic-auth

Configuration for HTTP

yaml
url: http://my.file.server/binaries/k0s-v1.30.1+k0s.0
sha256: e95603f167cce6e3cffef5594ef06785b3c1c00d3e27d8e4fc33824fe6c38a99
secretRef:
  namespace: kube-system
  name: artifacts-token-based-auth

Secrets Layout

The following standard Kubernetes secret types are supported:

Potentially supported in the future:

Moreover, k0s supports the following custom secret type:

  • k0sproject.io/http-authorization-header
    Sets a custom value for the HTTP Authorization header:

    yaml
    apiVersion: v1
    kind: Secret
    metadata:
      name: creds
      namespace: kube-system
    data:
      authorization: "Bearer abc123def456ghi789jkl0"

    The authorization entry is used as is, with its content placed directly into
    the Authorization header. A secret like the above will make Autopilot set
    the Authorization header to Bearer abc123def456ghi789jkl0.

Additional Details

  • The InsecureSkipTLSVerify property is equivalent to defining
    InsecureSkipTLSVerify on a Go HTTP client.
  • The InsecureSkipTLSVerify property will be valid for both oci:// and
    https:// protocols. It has no effect for the oci+http:// and http://
    protocols.
  • If a protocol is not specified or an incorrect one is provided, an error
    state should be activated.
  • If no SecretRef is 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 SecretRef does 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

  • All docs must live under the docs directory (I.E., changes to the main README.md are not reflected in the website).
  • Add a new link under nav in the main mkdocs.yml file:
yaml
# ... other directives
{%
    include "../../mkdocs.yml"
    start="# ~~~ START NAV SNIPPET ~~~\n"
    end="\n# ~~~ END NAV SNIPPET ~~~\n"
%}
  # more navigation links ...

Testing docs locally

We've got a dockerized setup for easily testing docs locally. Simply run
make 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

  1. Go to http://github.com/k0sproject/k0s
  2. On the top, right-hand side, click on "fork" and select your username for the fork destination.

Adding the Forked Remote

shell
export GITHUB_USER={ your github username }
shell
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 main

Your remotes should look something like this:

shell
git remote -v
shell
origin  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:

shell
git checkout -b my_feature_branch

Rebase your branch:

shell
git fetch origin && \
  git rebase origin/main
shell
Current 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:

shell
git commit --signoff

Commit 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:

text
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 via
git 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 by
git 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:

shell
git push --set-upstream my_fork my_feature_branch

You 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:

shell
git rebase -i origin/main

If your PR has 3 commits, output would be similar to this:

shell
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 out

Use 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:

shell
fixup <commit> = like "squash", but discard this commit's log message

Which 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:

shell
git push --force

You 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.

  1. Linting

    We use golangci-lint for style verification.
    In the repository's root directory, simply run:

    shell
    make lint

    There's no need to install golangci-lint manually. The build system will
    take care of that.

  2. Go fmt

    shell
    go fmt ./...
  3. Checking the documentation

    Follow the instructions for testing the documentation
    locally
    to verify any changes.

  4. Pre-submit Flight Checks

    In the repository root directory, make sure that:

    • make build && git diff --exit-code runs successfully.
      Verifies that the build is working and that the generated source code
      matches the one that's checked into source control.
    • make check-unit runs successfully.
      Verifies that all the unit tests pass.
    • make check-basic runs successfully.
      Verifies basic cluster functionality using one controller and two workers.
    • make check-hacontrolplane runs 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:

shell
make check-<name>

Examples:

shell
make check-basic
make check-ap-airgap

Some 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

  1. Download k0s-ansible

    Clone the k0s-ansible repository on your local machine:

    shell
    git clone https://github.com/movd/k0s-ansible.git
    cd k0s-ansible
  2. Create 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 7
    shell
    Create 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 LTS
  3. Create Ansible inventory

    1. Copy the sample to create the inventory directory:

      shell
      cp -rfp inventory/sample inventory/multipass
    2. Create the inventory.

      Assign the virtual machines to the different host groups, as required by the playbook logic.

      Host group Detail
      initial_controller Must contain a single node that creates the worker and controller tokens needed by the other nodes
      controller Can contain nodes that, together with the host from initial_controller, form a highly available isolated control plane
      worker Must contain at least one node, to allow for the deployment of Kubernetes objects
    3. Fill in inventory/multipass/inventory.yml. This can be done by direct entry using the metadata provided by multipass list,, or you can use the following Python script multipass_generate_inventory.py:

      shell
      ./tools/multipass_generate_inventory.py
      shell
      Designate first three instances as control plane
      Created Ansible Inventory at: /Users/dev/k0s-ansible/tools/inventory.yml
      $ cp tools/inventory.yml inventory/multipass/inventory.yml

      Your inventory/multipass/inventory.yml should 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
  4. Test the virtual machine connections

    Run the following command to test the connection to your hosts:

    shell
    ansible -i inventory/multipass/inventory.yml -m ping
    shell
    k0s-4 | SUCCESS => {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python3"
        },
        "changed": false,
        "ping": "pong"
    }
    ...

    If the test result indicates success, you can proceed.

  5. 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.

    shell
    ansible-playbook site.yml -i inventory/multipass/inventory.yml
    text

/* Detailed source-code truncated for AI context efficiency. */

text
## 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-info
shell
Kubernetes 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.3

Note: 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:

shell
ansible k0s-1 -a "k0s etcd member-list -c /etc/k0s/k0s.yaml" -i inventory/multipass/inventory.yml | tail -1 | jq
json
{
  "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.

shell
kubectl create deployment nginx --image=gcr.io/google-containers/nginx --replicas=5
shell
deployment.apps/nginx created
shell
kubectl expose deployment nginx --target-port=80 --port=8100
shell
service/nginx exposed
shell
kubectl run hello-k0s --image=quay.io/prometheus/busybox --rm -it --restart=Never --command -- wget -qO- nginx:8100
shell
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx on Debian!</title>
...
pod "hello-k0s" deleted

Note: 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:

shell
curl --proto '=https' --tlsv1.2 -sSf https://get.k0s.sh | sudo sh
sudo k0s install controller --single
sudo k0s start

Set kubeconfig

Next, you need to set the KUBECONFIG variable, which is needed by Flux CLI later on.

shell
sudo k0s kubeconfig admin > kubeconfig
export KUBECONFIG=$PWD/kubeconfig

Install 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:

shell
brew install fluxcd/tap/flux

or

shell
curl --proto '=https' --tlsv1.2 -sSf https://fluxcd.io/install.sh | sudo bash

For 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:

shell
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.

shell
export GITHUB_REPO_NAME=<select-repo-name-to-be-created>

Bootstrap flux to your cluster. The GitHub repo will be created automatically by Flux:

shell
flux bootstrap github \
--owner=$GITHUB_USER \
--repository=$GITHUB_REPO_NAME \
--branch=main \
--path=./clusters/my-cluster \
--personal

Now 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:

shell
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:

yaml
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: 30003

Then push the new file to the repository:

shell
git add .
git commit -m "Add web server manifest"
git push

Check that Flux detects your changes and the web server gets applied (by default this should happen within 1 min):

shell
flux get kustomizations

If the deployment went successfully, you should see the newly added objects:

shell
sudo k0s kubectl get all -n web

You can try to access the web application using

shell
curl localhost:30003

or 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:

shell
flux uninstall --namespace=flux-system

Your 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:

sh
sudo apt-get install -y open-iscsi

RHEL/CentOS/Rocky:

sh
sudo yum install -y iscsi-initiator-utils

SUSE:

sh
sudo zypper install -y open-iscsi

Ensure the iSCSI daemon is enabled and running:

sh
sudo systemctl enable --now iscsid

Install 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:

yaml
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.1

Verification Steps

After applying the configuration, verify that Longhorn is properly installed:

Check pod status: Wait for all Longhorn components to become ready:

sh
k0s kubectl -n longhorn-system get pods

Expected output should show all pods in Running status. This may take 2-3 minutes.

Verify StorageClass: Confirm that the longhorn StorageClass was created:

sh
$ 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                   16m

You 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).

yaml
spec:
  extensions:
    helm:
      charts:
        - name: longhorn
          chartname: longhorn/longhorn
          namespace: longhorn-system
          version: "latest"
          values: |
            defaultSettings:
              defaultReplicaCount: 1

Access the Longhorn UI

You can port-forward the frontend service:

sh
k0s kubectl -n longhorn-system port-forward svc/longhorn-frontend 8080:80

Then 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.

yaml
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-pvc

Apply the manifest:

sh
k0s kubectl apply -f nginx.yaml

Check the PVC and pod status:

sh
k0s kubectl get pvc,pods

Next Steps

Longhorn supports backing up volumes to external storage systems such as S3, NFS, or CIFS. To enable backups:

  1. Create a backup target secret with your storage credentials
  2. Configure the backup target URL in Longhorn settings
  3. 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


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):

yaml
apiVersion: k0s.k0sproject.io/v1beta1
kind: ClusterConfig

metadata:
  name: k0s

spec:
  network:
    kubeProxy:
      mode: ipvs
      ipvs:
        strictARP: true

Port 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

  1. Install MetalLB using the official Helm chart and k0s Helm extension manager:

    yaml
    apiVersion: 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-system

    Other installation methods are available in the MetalLB's official documentation.

  2. 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.yaml

    yaml
    ---
    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-system
  3. Deploy an example application (web server) with a load balancer

    yaml
    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:
      selector:
        app: web
      ports:
        - protocol: TCP
          port: 80
          targetPort: 80
      type: LoadBalancer
  4. Check your LoadBalancer

    Run the following command to see your LoadBalancer with the external-ip and port.

    shell
    kubectl get service -n web
  5. Access 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.

    shell
    curl <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.

  1. Install Ingress NGINX Controller (using the official manifests from the official ingress-nginx Kubernetes git repository)

    shell
    kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.1.3/deploy/static/provider/baremetal/deploy.yaml
  2. Check that the Ingress controller pods have started

    shell
    kubectl get pods -n ingress-nginx
  3. Check that you can see the NodePort service

    shell
    kubectl get services -n ingress-nginx
  4. From version v1.0.0 of the Ingress NGINX Controller, a ingressclass object is required.

    In the default installation, an ingressclass object named nginx has already been created.

    shell
    $ kubectl -n ingress-nginx get ingressclasses
    NAME    CONTROLLER             PARAMETERS   AGE
    nginx   k8s.io/ingress-nginx   <none>       162m

    If this is only instance of the Ingress NGINX Controller, you should add the annotation ingressclass.kubernetes.io/is-default-class in your ingress class:

    shell
    kubectl -n ingress-nginx annotate ingressclasses nginx ingressclass.kubernetes.io/is-default-class="true"
  5. Try connecting the controller using the NodePort from the previous step (in the range of 30000-32767)

    shell
    curl <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.

  6. 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:

    yaml
    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:
      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: 5000

    Deploy the app:

    shell
    kubectl apply -f simple-web-server-with-ingress.yaml
  7. Verify that you can access your application using the NodePort from step 3.

    shell
    curl <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.

  1. Install a LoadBalancer

    There are two alternatives to install LoadBalancer on k0s. Follow the links in order to install LoadBalancer.

    • MetalLB as a pure software solution running internally in the k0s cluster
    • Cloud provider's load balancer running outside of the k0s cluster
  2. Verify LoadBalancer

    In 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.

    yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: example-load-balancer
    spec:
      selector:
        app: web
      ports:
        - protocol: TCP
          port: 80
          targetPort: 80
      type: LoadBalancer
    shell
    kubectl apply -f example-load-balancer.yaml

    Then run the following command to see your LoadBalancer with an external IP address.

    shell
    kubectl get service example-load-balancer

    If the LoadBalancer is 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:

    shell
    kubectl delete -f example-load-balancer.yaml
  3. Install Ingress NGINX Controller by following the steps in the previous chapter (step 1 to step 4).

  4. Edit the Ingress NGINX Controller to use LoadBalancer instead of NodePort

    shell
    kubectl edit service ingress-nginx-controller -n ingress-nginx

    Find the spec.type field and change it from NodePort to LoadBalancer.

  5. Check that you can see the ingress-nginx service with type LoadBalancer.

    shell
    kubectl get services -n ingress-nginx
  6. Try 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.

    shell
    curl <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.

  7. Deploy a small test application (Apache HTTP Server) to verify your Ingress.

    Create the YAML file simple-web-server-with-ingress.yaml as described in the previous chapter (step 6) and deploy it.

    shell
    kubectl apply -f simple-web-server-with-ingress.yaml
  8. Verify that you can access your application through the LoadBalancer and ingress controller.

    shell
    curl <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.

  1. Install Ingress NGINX Controller manifests:

    shell
    wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.1.3/deploy/static/provider/baremetal/deploy.yaml
  2. Edit deploy.yaml. Find the Deployment ingress-nginx-controller and enable the host network option by adding the hostNetwork line:

    shell
    spec:
      template:
        spec:
          hostNetwork: true

    You can also remove the Service ingress-nginx-controller completely, because it won't be needed.

  3. Install Ingress

    shell
    kubectl apply -f deploy.yaml
  4. Try 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

Traefik Ingress


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.

Chart in Artifacthub

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.

yaml
lvm-localpv:
  lvmNode:
    kubeletDir: /var/lib/k0s/kubelet
zfs-localpv:
  zfsNode:
    kubeletDir: /var/lib/k0s/kubelet
mayastor:
  csi:
    node:
      kubeletDir: /var/lib/k0s/kubelet

Storage 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

yaml
engines:
  local:
    lvm:
      enabled: true
    rawfile:
      enabled: true
    zfs:
      enabled: true
    rawfile:
        enabled: false
  replicated:
    mayastor:
      enabled: true

Monitoring

Per default the chart also installs Loki, Minio and Alloy for logging purposes.
If you do not need these you can disable them.

yaml
alloy:
  enabled: false
loki:
  enabled: false
  minio:
    enabled: false

Local storage only variant

yaml
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: false

Local + replicated storage variant

Be sure to read the prerequisites of OpenEBS for distributed storage, as some host dependencies are needed.

yaml
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: false

Set 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).

shell
k0s kubectl get storageclass
shell
NAME                         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                   76m

The 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:

yaml
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-pvc
shell
k0s kubectl apply -f nginx.yaml
shell
persistentvolumeclaim/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          30s
shell
k0s kubectl get pv
shell
NAME                                       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            30s

Examples 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.

sh
git clone [email protected]:k0sproject/k0sctl.git

Take a look at the Terraform files

sh
cd k0sctl/examples/aws-tf
ls -l

Open variables.tf and set the number of controller and worker nodes like this:

terraform
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.

shell
terraform init
terraform apply

If 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:

shell
lsblk -f
shell
NAME        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% /
nvme1n1

The 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.

shell
terraform output -raw k0s_cluster > k0sctl.yaml

After that deploying k0s becomes very easy with the ready-made configuration.

shell
k0sctl apply --config k0sctl.yaml

It 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.

shell
k0sctl kubeconfig --config k0sctl.yaml > kubeconfig
export KUBECONFIG=$PWD/kubeconfig
kubectl get nodes

The 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).

shell
ssh -i aws.pem <username>@<ip-address>
sudo k0s kubectl get nodes

6. Deploy Rook

To get started with Rook, let's first clone the Rook GitHub repo:

shell
git clone --single-branch --branch release-1.7 https://github.com/rook/rook.git
cd rook/cluster/examples/kubernetes/ceph

We will use mostly the default Rook configuration. However, k0s kubelet directory must be configured in operator.yaml like this

yaml
ROOK_CSI_KUBELET_DIR_PATH: "/var/lib/k0s/kubelet"

To create the resources, which are needed by the Rook’s Ceph operator, run

shell
kubectl apply -f crds.yaml -f common.yaml -f operator.yaml

Now you should see the operator running. Check them with

shell
kubectl get pods -n rook-ceph

7. Deploy Ceph Cluster

Then you can proceed to create a Ceph cluster. Ceph will use the three EBS volumes attached to the worker nodes:

shell
kubectl apply -f cluster.yaml

It takes some minutes to prepare the volumes and create the cluster. Once this is completed you should see the following output:

shell
kubectl get pods -n rook-ceph
text
/* 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.

shell
kubectl apply -f ./csi/rbd/storageclass.yaml

9. Request storage

Create a new manifest file mongo-pvc.yaml with the following content:

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongo-pvc
spec:
  storageClassName: rook-ceph-block
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi

This 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.

shell
kubectl apply -f mongo-pvc.yaml

You can now check the status of your PVC:

shell
kubectl get pvc

When the PVC gets the requested volume reserved (bound), it should look like this:

shell
kubectl get pvc
shell
NAME        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS      AGE
mongo-pvc   Bound    pvc-08337736-65dd-49d2-938c-8197a8871739   2Gi        RWO            rook-ceph-block   6s

10. 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:

yaml
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-pvc

Deploy the database:

shell
kubectl apply -f mongo.yaml

11. Access the application

Open the MongoDB shell using the MongoDB pod:

shell
kubectl get pods
shell
NAME                    READY   STATUS    RESTARTS   AGE
mongo-b87cbd5cc-4wx8t   1/1     Running   0          76s
shell
kubectl exec -it mongo-b87cbd5cc-4wx8t -- mongo

Create a DB and insert some data:

mongodb
> 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:

mongodb
> 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:

shell
terraform destroy

Remember 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.

yaml
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-system

Configure 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:

yaml
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:

yaml
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

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-system

Retrieve 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.

shell
kubectl get all

Output:

shell
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       81s

Take 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:

shell
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   84s

Receiving a 404 response here is normal, as you've not configured any Ingress resources to respond yet:

shell
curl http://192.168.0.5
shell
404 page not found

Deploy 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).

  1. Create the Traefik Dashboard IngressRoute in a YAML file:

    yaml
    apiVersion: 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: TraefikService
  2. Deploy the resource:

    shell
    kubectl apply -f traefik-dashboard.yaml

    Output:

    shell
    ingressroute.traefik.io/v1alpha1/traefik-dashboard created

    At this point you should be able to access the dashboard using the EXTERNAL-IP that you noted above by visiting http://192.168.0.5/dashboard/ in your browser:

  3. Create a simple whoami Deployment, Service, and Ingress manifest:

    yaml
    apiVersion: 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: 80
  4. Apply the manifests:

    shell
    kubectl apply -f whoami.yaml

    Output:

    shell
    deployment.apps/whoami-deployment created
    service/whoami-service created
    ingress.networking.k8s.io/whoami-ingress created
  5. Test the ingress and service:

    shell
    curl http://192.168.0.5/whoami

    Output:

    shell
    Hostname: 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:

  1. run ./hack/get-calico.sh <version>
  2. check the git diff to see if it looks sensible
  3. re-apply our manual adjustments (documented below)
  4. compile, pray, and test
  5. 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:

sh
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 (the
cni_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:

yaml
# 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

yaml
# Auto detect the iptables backend
- name: FELIX_IPTABLESBACKEND
  value: "auto"

Support for enabling WireGuard

yaml
{{- if .EnableWireguard }}
- name: FELIX_WIREGUARDENABLED
  value: "true"
{{- end }}

Support for changing the cluster CIDR

yaml
- name: CALICO_IPV4POOL_CIDR
  value: "{{ .ClusterCIDR }}"

Support for changing the MTU

Search for .MTU to find:

yaml
# 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:

yaml
- name: CLUSTER_TYPE
  value: "k8s"

Disable BIRD checks on liveness and readiness probes when not running the BIRD
backend:

yaml
{{- if eq .Mode "bird" }}
- -bird-live
{{- end }}
yaml
{{- 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.

yaml
- name: FELIX_FEATUREDETECTOVERRIDE
  value: ChecksumOffloadBroken=true

Template container image names

Instead of hardcoded image names and versions use placeholders to support
configuration level settings. Following placeholders are used:

  • CalicoCNIImage for calico/cni
  • CalicoNodeImage for calico/node
  • CalicoKubeControllersImage for calico/kube-controllers

Also, all containers in manifests were modified to have the imagePullPolicy
field:

yaml
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:

shell
k0s controller --single

See 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:

shell
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:

  1. Take a backup! Things might go wrong at any level.

  2. 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.

  3. 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-worker flag:

    • /var/lib/k0s/kubelet.conf
    • /var/lib/k0s/kubelet/pki
  4. Choose one controller as the "first" one. Restart k0s on the first
    controller. If this controller is running with the --enable-worker flag,
    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.

  5. 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.

  6. Rejoin all workers. The easiest way to do this is to use a
    kubelet-bootstrap.conf file. You can generate
    such a file on a controller like this (see the section on join tokens for
    details):

    sh
    touch /tmp/rejoin-token &&
      chmod 0600 /tmp/rejoin-token &&
      k0s token create --expiry 1h |
      base64 -d |
      gunzip >/tmp/rejoin-token

    Copy that token to each worker node and place it at
    /var/lib/k0s/kubelet-bootstrap.conf. Then reboot the machine.

  7. When all worker nodes are back online, the kubelet-bootstrap.conf files can
    be safely removed from the worker nodes. The token can be invalidated without
    waiting for its expiration: Use k0s token list --role worker to list all tokens and k0s 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:

text
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=stderr

Where are the logs?

systemd based setups

systemd stores logs in its journal. Access k0s and all component logs using
journalctl. 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. The
sbctl 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-system namespace
  • 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:

shell
support-bundle --kubeconfig /var/lib/k0s/pki/admin.conf https://docs.k0sproject.io/stable/support-bundle-<role>.yaml

Above <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:

shell
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.yaml

Once the data has been collected, a file named
support-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:

console
$ 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          167m

When you check the logs, it'll show something like this:

shell
kubectl -n kube-system logs coredns-5c98d7d4d8-tfs4q
shell
plugin/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 variable
ETCD_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 ls that the containerd ZFS snapshotter plugin is in ok state (should be the case if ZFS kernel modules and ZFS userspace utils are correctly configured):
console
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:
toml
...
    [plugins.'io.containerd.cri.v1.images']
      snapshotter = "overlayfs"
...

to

toml
...
    [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:

yaml
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

shell
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:

shell
DEBUG=true make k0s

Any value not equal to the "false" would work.

To add custom linker flags use LDFLAGS variable.

shell
LD_FLAGS="--custom-flag=value" make k0s

Using 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

yaml
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 username

Use 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:

yaml
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: default
  name: dev-role
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]

RoleBinding example:

yaml
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.io

The 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:

bash
k8s-oidc-helper --client-id=<CLIENT_ID> \
  --client-secret=<CLIENT_SECRET> \
  --write=true

Using 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`

bash
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:

console
$ 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:

console
$ 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-cdi

Further 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:

console
$ 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-slicing

Further 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:

console
$ 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 Found

Further 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:

console
$ 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:

shell
helm repo add autoscaler https://kubernetes.github.io/autoscaler
helm install cluster-autoscaler autoscaler/cluster-autoscaler \
  --namespace kube-system \
  --set cloudProvider=azure

Further 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:

console
$ 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     Pending

Further 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:

console
$ 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:

console
$ 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:

console
$ 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     Pending

Further 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:

console
$ 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