First, we need to understand how exactly it works.
In short, for all nodes we have prepared the image with the OS, Docker, Kubelet and everything else that you need there. This image with the kernel is building automatically by CI using Dockerfile. End nodes are booting the kernel and OS from this image via the network.
Nodes are using overlays as the root filesystem and after reboot any changes will be lost (like in Docker containers). You have a config-file where you can describe mounts and some initial commands which should be executed during node boot (Example: set root user ssh-key and kubeadm join commands)
## Image Preparation Process
We will use LTSP project because it's gives us everything we need to organize the network booting environment. Basically, LTSP is a pack of shell-scripts which makes our life much easier.
LTSP provides a initramfs module, a few helper-scripts, and the configuration system which prepare the system during the early state of boot, before the main init process call.
**This is what the image preparation procedure looks like:**
- You're deploying the basesystem in the chroot environment.
- Make any needed changes there, install software.
- Run the `ltsp-build-image` command
After that, you will get the squashed image from the chroot with all the software inside. Each node will download this image during the boot and use it as the rootfs. For the update node, you can just reboot it. The new squashed image will be downloaded and mounted into the rootfs.
## Server Components
**The server part of LTSP includes two components in our case:**
- **NBD-server** - NBD protocol is used to distribute the squashed rootfs image to the clients. It is the fastest way, but if you want, it can be replaced by the NFS or AoE protocol.
You should also have:
- **DHCP-server** - it will distribute the IP-settings and a few specific options to the clients to make it possible for them to boot from our LTSP-server.
## Node Booting Process
**This is how the node is booting up**
- The first time, the node will ask DHCP for IP-settings and `next-server`, `filename` options.
- Next, the node will apply settings and download bootloader (pxelinux or grub)
- Bootloader will download and read config with the kernel and initramfs image.
- Then bootloader will download the kernel and initramfs and execute it with specific cmdline options.
- During the boot, initramfs modules will handle options from cmdline and do some actions like connect NBD-device, prepare overlay rootfs, etc.
- Afterwards it will call the ltsp-init system instead of the normal init.
- ltsp-init scripts will prepare the system on the earlier stage, before the main init will be called. Basically it applies the setting from lts.conf (main config): write fstab and rc.local entries etc.
- Call the main init (systemd) which is booting the configured system as usual, mounts shares from fstab, start targets and services, executes commands from rc.local file.
- In the end you have a fully configured and booted system ready for further operations.
# Preparing the Server
As I said before, I'm preparing the LTSP-server with the squashed image automatically using Dockerfile. This method is quite good because you have all steps described in your git repository.
You have versioning, branches, CI and everything that you used to use for preparing your usual Docker projects.
Otherwise, you can deploy the LTSP server manually by executing all steps by hand. This is a good practice for learning and understanding the basic principles.
Just repeat all the steps listed here by hand, just to try to install LTSP without Dockerfile.
## Used Patches List
LTSP still has some issues which authors don’t want to apply, yet. However LTSP is easy customizable so I prepared a few patches for myself and will share them here.
I’ll create a fork if the community will warmly accept my solution.
This patch adds a PREINIT option to lts.conf, which allows you to run custom commands before the main init call. It may be useful to modify the systemd units and configure the network. It's remarkable that all environment variables from the boot environment are saved and you can use them in your scripts.
This is not a patch but a special wrapper script which allows you to run NBD-server in the foreground. It is useful if you want to run it inside a Docker container.
## Dockerfile Stages
We will use [stage building](https://docs.docker.com/develop/develop-images/multistage-build/) in our Dockerfile to leave only the needed parts in our Docker image. The unused parts will be removed from the final image.
```
ltsp-base
(install basic LTSP server software)
|
|---basesystem
| (prepare chroot with main software and kernel)
| |
| |---builder
| | (build additional software from sources, if needed)
| |
| '---ltsp-image
| (install additional software, docker, kubelet and build squashed image)
|
'---final-stage
(copy squashed image, kernel and initramfs into first stage)
```
### Stage 1: ltsp-base
Let's start writing our Dockerfile. This is the first part:
At this stage our Docker image has already been installed:
* NBD-server
* TFTP-server
* LTSP-scripts with grub bootloader support (for EFI)
### Stage 2: basesystem
In this stage we will prepare a chroot environment with basesystem, and install basic software with the kernel.
We will use the classic **debootstrap** instead of **ltsp-build-client** to prepare the base image, because **ltsp-build-client** will install GUI and few other things which we don't need for the server deployment.
```Dockerfile
FROM ltsp-base as basesystem
ARG DEBIAN_FRONTEND=noninteractive
# Prepare base system
RUN debootstrap --arch amd64 xenial /opt/ltsp/amd64
# Install updates
RUN echo "\
deb http://archive.ubuntu.com/ubuntu xenial main restricted universe multiverse\n\
deb http://archive.ubuntu.com/ubuntu xenial-updates main restricted universe multiverse\n\
deb http://archive.ubuntu.com/ubuntu xenial-security main restricted universe multiverse" \
> /opt/ltsp/amd64/etc/apt/sources.list \
&& ltsp-chroot apt-get -y update \
&& ltsp-chroot apt-get -y upgrade
# Installing LTSP-packages
RUN ltsp-chroot apt-get -y install ltsp-client-core
# Apply initramfs patches
# 1: Read params from /etc/lts.conf during the boot (#1680490)
# 2: Add support for PREINIT variables in lts.conf
ADD /patches /patches
RUN patch -p4 -d /opt/ltsp/amd64/usr/share < /patches/feature_initramfs_params_from_lts_conf.diff \
Unfortunately, we can't use the standard Kubernetes service abstraction for our deployment, because TFTP can't work behind the NAT. During the boot, our nodes are not part of Kubernetes cluster and they requires ExternalIP, but Kubernetes always enables NAT for ExternalIPs, and there is no way to override this behavior.
For now I have two ways for avoid this: use `hostNetwork: true` or use [pipework](https://github.com/dreamcat4/docker-images/blob/master/pipework/3.%20Examples.md#kubernetes). The second option will also provide you redundancy because, in case of failure, the IP will be moved with the Pod to another node. Unfortunately, pipework is not native and a less secure method.
If you have some better option for that please let me know.
* **KEEP_SYSTEM_SERVICES** - during the boot, LTSP automatically removes some services, this variable is needed to prevent this behavior.
* **PREINIT_*** - commands listed here will be executed before systemd runs (this function was added by the [feature_preinit.diff](#used-patches-list) patch)
* **FSTAB_*** - entries written here will be added to the `/etc/fstab` file.
As you can see, I use the `nofail` option, that means that if a partition doesn't exist, it will continue to boot without error.
If you have fully diskless nodes you can remove the FSTAB settings or configure the remote filesystem there.
* **RCFILE_*** - those commands will be written to `rc.local` file, which will be called by systemd during the boot.
Here I load the kernel modules and add some sysctl tunes, then call the `kubeadm join` command, which adds my node to the Kubernetes cluster.
You can get more details on all the variables used from [lts.conf manpage](http://manpages.ubuntu.com/manpages/xenial/man5/lts.conf.5.html).
Now you can configure your DHCP. Basically you should set the `next-server` and `filename` options.
I use ISC-DHCP server, and here is an example `dhcpd.conf`:
```
shared-network ltsp-netowrk {
subnet 10.9.0.0 netmask 255.255.0.0 {
authoritative;
default-lease-time -1;
max-lease-time -1;
option domain-name "example.org";
option domain-name-servers 10.9.0.1;
option routers 10.9.0.1;
next-server ltsp-1; # write LTSP-server hostname here
if option architecture = 00:07 {
filename "/ltsp/amd64/grub/x86_64-efi/core.efi";
} else {
filename "/ltsp/amd64/grub/i386-pc/core.0";
}
range 10.9.200.0 10.9.250.254;
}
```
You can start from this, but what about me, I have multiple LTSP-servers and I configure leases statically for each node via the Ansible playbook.
Try to run your first node. If everything was right, you will have a running system there.
The node also will be added to your Kubernetes cluster.
Now you can try to make your own changes.
If you need something more, note that LTSP can be easily changed to meet your needs.
Feel free to look into the source code and you can find many answers there.
_I can answer. The main feature here is image preparation process, not configuration. In case with LTSP you have classic Ubuntu system, and everything that can be installed on Ubuntu it can also be written here in the Dockerfile. In case CoreOS you have no so many freedom and you can’t easily add custom kernel modules and packages at the build stage of the boot image._