
Neste artigo, gostaria de mostrar uma tecnologia interessante, que eu a uso com sucesso no Kubernetes. Pode ser realmente útil para criar grandes agrupamentos.
A partir de agora, você não precisa mais pensar em instalar o SO e separar pacotes para cada nó. Porque Você pode fazer tudo isso automaticamente através do Dockerfile!
O fato de você poder comprar centenas de novos servidores, adicioná-los ao seu ambiente de trabalho e quase imediatamente prepará-los para o uso é realmente incrível!
Intrigado? Agora vamos falar sobre tudo em ordem.
Sumário
Para começar, precisamos entender exatamente como esse circuito funciona.
Resumidamente, para todos os nós, estamos preparando uma imagem única com o SO, Docker, Kubelet e tudo mais.
Essa imagem do sistema, juntamente com o kernel, é criada automaticamente pelo CI usando o Dockerfile.
Os nós finais carregam o SO e o kernel desta imagem diretamente através da rede.
Os nós usam overlayfs como o sistema de arquivos raiz, portanto, no caso de uma reinicialização, todas as alterações serão perdidas (assim como no caso de contêineres do docker).
Há uma configuração principal, nela você pode descrever os pontos de montagem e alguns comandos que devem ser executados quando o nó for carregado (por exemplo, um comando para adicionar uma chave ssh e kubeadm join
)
Processo de preparação da imagem
Usaremos o projeto LTSP porque ele nos fornece tudo o que precisamos para organizar a inicialização da rede.
Em geral, o LTSP é um pacote de scripts de shell que facilita muito a nossa vida.
Ele fornece um módulo initramfs, vários scripts auxiliares e algum tipo de sistema de configuração que prepara o sistema em um estágio inicial de carregamento, mesmo antes de chamar o init.
É assim que o procedimento de preparação da imagem se parece:
- Implante o sistema base em um ambiente chroot.
- Fazemos as alterações necessárias, instalamos o software.
- Execute o comando
ltsp-build-image
Logo depois disso, você obterá uma imagem compactada deste chroot com todo o software instalado dentro.
Cada nó fará o download desta imagem no momento da inicialização e a utilizará como rootfs.
Para atualizar, basta reiniciar o nó, a nova imagem será baixada e usada para rootfs.
Componentes do servidor
A parte do servidor do LTSP, no nosso caso, inclui apenas dois componentes:
- Servidor TFTP - TFTP é um protocolo de inicialização, é usado para carregar o kernel, o initramfs e o principal config-lts.conf.
- Servidor NBD - protocolo NBD usado para entregar uma imagem rootfs compactada aos clientes. Este é o método mais rápido, mas, se desejado, pode ser substituído por NFS ou AoE.
Você também precisa ter:
- Servidor DHCP - distribuirá a configuração de IP e várias opções adicionais necessárias para que nossos clientes possam inicializar a partir do servidor LTSP.
Processo de carregamento do nó
Descrição do processo de carregamento do nó
- Primeiro, o nó solicitará o endereço IP do DHCP e as opções para o
next-server
, filename
. - Em seguida, o nó aplicará as configurações e fará o download do gerenciador de inicialização (pxelinux ou grub)
- O gerenciador de inicialização fará o download e carregará a configuração com o kernel e o initramfs.
- Em seguida, ele carregará o kernel e o initramfs com opções específicas especificadas para o kernel.
- No momento da inicialização, os módulos initramfs processam os parâmetros do cmdline e executam algumas ações, como conectar um dispositivo nbd, preparar rootfs de sobreposição etc.
- Depois disso, em vez do init usual, um ltsp-init especial será chamado.
- Os scripts ltsp-init prepararão o sistema desde o início antes que o init principal seja chamado. Basicamente, as opções do lts.conf (o arquivo de configuração principal) são usadas aqui: isso está atualizando os registros no fstab e rc.local, etc.
- Em seguida, haverá uma chamada para o init principal (systemd), que carregará o sistema já configurado como de costume, monte recursos compartilhados do fstab, inicie destinos e serviços e execute comandos do rc.local.
- Como resultado, obtemos um sistema totalmente configurado e carregado, pronto para mais ações.
Preparação do servidor
Como eu disse, eu preparo o servidor LTSP com a imagem compactada automaticamente usando o Dockerfile. Este método não é ruim, porque todas as etapas para construção podem ser descritas em seu repositório git. Você pode controlar versões, usar tags, aplicar IC e tudo o que você usaria para a preparação dos projetos habituais do Docker.
Por outro lado, você pode implantar o servidor LTSP manualmente, seguindo todas as etapas manualmente, isso pode ser bom para propósitos de treinamento e para entender os princípios básicos.
Execute os comandos listados no artigo manualmente se você quiser experimentar o LTSP sem o Dockerfile.
Lista de patches usados
No momento, o LTSP tem algumas falhas e os autores do projeto não estão muito dispostos a aceitar correções. Felizmente, o LTSP é facilmente personalizável, então eu preparei alguns patches para mim, vou dar aqui.
Talvez um dia eu amadureça na bifurcação se a comunidade aceitar calorosamente minha decisão.
- feature-grub.diff
Por padrão, o LTSP não suporta EFI, por isso preparei um patch que adiciona GRUB2 com suporte a EFI. - feature_preinit.diff
Este patch adiciona a opção PREINIT ao lts.conf, que permite executar comandos arbitrários antes de chamar o init principal. Isso pode ser útil para modificar unidades do sistema e configurações de rede. Vale ressaltar que todas as variáveis do ambiente de inicialização são salvas e você pode usá-las em seus scripts chamados por meio dessa opção. - feature_initramfs_params_from_lts_conf.diff
Resolve o problema com a opção NBD_TO_RAM desativada. Após esse patch, você pode especificá-lo no lts.conf dentro do chroot. (não aquele no diretório tftp) - nbd-server-wrapper.sh
Isso não é um patch, mas apenas um script de shell, ele permite executar o servidor nbd no foregroud; será necessário se você deseja executar o servidor nbd dentro do contêiner do Docker.
Estágios do Dockerfile
Usaremos a criação de palco em nosso Dockerfile para salvar apenas as partes necessárias da nossa imagem do Docker, as demais partes não utilizadas serão excluídas da imagem final.
ltsp-base ( ltsp ) | |---basesystem | ( chroot- ) | | | |---builder | | ( , ) | | | '---ltsp-image | ( , docker, kubelet squashed ) | '---final-stage ( squashed , initramfs stage)
Estágio 1: ltsp-base
OK, vamos começar, esta é a primeira parte do nosso Dockerfile:
FROM ubuntu:16.04 as ltsp-base ADD nbd-server-wrapper.sh /bin/ ADD /patches/feature-grub.diff /patches/feature-grub.diff RUN apt-get -y update \ && apt-get -y install \ ltsp-server \ tftpd-hpa \ nbd-server \ grub-common \ grub-pc-bin \ grub-efi-amd64-bin \ curl \ patch \ && sed -i 's|in_target mount|in_target_nofail mount|' \ /usr/share/debootstrap/functions \
No momento, nossa imagem do Docker já possui o seguinte instalado:
- Servidor NBD
- Servidor TFTP
- Scripts LTSP com suporte ao gerenciador de inicialização grub (para EFI)
Etapa 2: sistema de base
Nesta fase, prepararemos o ambiente chroot com o sistema base e instalaremos o software principal com o kernel.
Usaremos o debootstrap usual em vez do ltsp-build-client para preparar a imagem, porque o ltsp-build-client instalará a GUI e outras coisas desnecessárias que obviamente não serão úteis para implantar servidores.
FROM ltsp-base as basesystem ARG DEBIAN_FRONTEND=noninteractive
Observe que alguns pacotes, como o lvm2, podem ter problemas. Eles não são totalmente otimizados para instalação em um chroot sem privilégios. Seus scripts pós-instalação tentam chamar comandos privilegiados que podem falhar e bloquear a instalação de todo o pacote.
Solução:
- Alguns podem ser instalados sem problemas se você os instalar antes de instalar o kernel (por exemplo, lvm2)
- Mas para alguns deles, você precisará usar esta solução alternativa para instalar sem um script pós-instalação.
Etapa 3: construtor
Nesta fase, podemos coletar todos os softwares e módulos de kernel necessários das fontes, é muito legal que seja possível fazer isso nesta fase, no modo totalmente automático.
Pule esta etapa se você não precisar coletar nada dos artistas.
Vou dar um exemplo de instalação da versão mais recente do driver MLNX_EN:
FROM basesystem as builder
Etapa 4: imagem ltsp
Nesta fase, estabeleceremos o que coletamos na etapa anterior:
FROM basesystem as ltsp-image
Agora, faremos alterações adicionais para concluir nossa imagem LTSP:
Agora faça uma imagem quadriculada do nosso chroot:
Etapa 5: Etapa final
Na fase final, salvamos apenas nossa imagem e núcleo compactados com initramfs
FROM ltsp-base COPY --from=ltsp-image /opt/ltsp/images /opt/ltsp/images COPY --from=ltsp-image /etc/nbd-server/conf.d /etc/nbd-server/conf.d COPY --from=ltsp-image /var/lib/tftpboot /var/lib/tftpboot
Ok, agora temos uma imagem do docker que inclui:
- Servidor TFTP
- Servidor NBD
- carregador de inicialização configurado
- kernel com initramfs
- imagem de rootfs esmagada
Use
OK, agora que nossa imagem do Docker com o servidor LTSP, kernel, initramfs e rootfs esmagados está completamente pronta, podemos executar a implantação com ela.
Podemos fazer isso como sempre, mas há outra questão que precisamos resolver.
Infelizmente, não podemos usar o serviço Kubernetes usual para nossa implantação, porque no momento da inicialização os nós não fazem parte do cluster Kubernetes e precisam usar o externalIP, mas o Kubernetes sempre usa o NAT para externalIP e, atualmente, não há como alterar esse comportamento.
Conheço duas maneiras de evitar isso: use hostNetwork: true
ou use pipework , a segunda opção também nos fornecerá tolerância a falhas, como em caso de falha, o endereço IP será movido para outro nó com o contêiner. Infelizmente, a tubulação não é um método nativo e menos seguro.
Se você souber de alguma solução mais adequada, informe-nos.
Aqui está um exemplo de implantação com hostNetwork:
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: ltsp-server labels: app: ltsp-server spec: selector: matchLabels: name: ltsp-server replicas: 1 template: metadata: labels: name: ltsp-server spec: hostNetwork: true containers: - name: tftpd image: registry.example.org/example/ltsp:latest command: [ "/usr/sbin/in.tftpd", "-L", "-u", "tftp", "-a", ":69", "-s", "/var/lib/tftpboot" ] lifecycle: postStart: exec: command: ["/bin/sh", "-c", "cd /var/lib/tftpboot/ltsp/amd64; ln -sf config/lts.conf ." ] volumeMounts: - name: config mountPath: "/var/lib/tftpboot/ltsp/amd64/config" - name: nbd-server image: registry.example.org/example/ltsp:latest command: [ "/bin/nbd-server-wrapper.sh" ] volumes: - name: config configMap: name: ltsp-config
Como você deve ter notado, o configmap com o arquivo lts.conf também é usado aqui.
Como exemplo, darei parte da minha configuração:
apiVersion: v1 kind: ConfigMap metadata: name: ltsp-config data: lts.conf: | [default] KEEP_SYSTEM_SERVICES = "ssh ureadahead dbus-org.freedesktop.login1 systemd-logind polkitd cgmanager ufw rpcbind nfs-kernel-server" PREINIT_00_TIME = "ln -sf /usr/share/zoneinfo/Europe/Prague /etc/localtime" PREINIT_01_FIX_HOSTNAME = "sed -i '/^127.0.0.2/d' /etc/hosts" PREINIT_02_DOCKER_OPTIONS = "sed -i 's|^ExecStart=.*|ExecStart=/usr/bin/dockerd -H fd:// --storage-driver overlay2 --iptables=false --ip-masq=false --log-driver=json-file --log-opt=max-size=10m --log-opt=max-file=5|' /etc/systemd/system/docker.service" FSTAB_01_SSH = "/dev/data/ssh /etc/ssh ext4 nofail,noatime,nodiratime 0 0" FSTAB_02_JOURNALD = "/dev/data/journal /var/log/journal ext4 nofail,noatime,nodiratime 0 0" FSTAB_03_DOCKER = "/dev/data/docker /var/lib/docker ext4 nofail,noatime,nodiratime 0 0" # Each command will stop script execution when fail RCFILE_01_SSH_SERVER = "cp /rofs/etc/ssh/*_config /etc/ssh; ssh-keygen -A" RCFILE_02_SSH_CLIENT = "mkdir -p /root/.ssh/; echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBSLYRaORL2znr1V4a3rjDn3HDHn2CsvUNK1nv8+CctoICtJOPXl6zQycI9KXNhANfJpc6iQG1ZPZUR74IiNhNIKvOpnNRPyLZ5opm01MVIDIZgi9g0DUks1g5gLV5LKzED8xYKMBmAfXMxh/nsP9KEvxGvTJB3OD+/bBxpliTl5xY3Eu41+VmZqVOz3Yl98+X8cZTgqx2dmsHUk7VKN9OZuCjIZL9MtJCZyOSRbjuo4HFEssotR1mvANyz+BUXkjqv2pEa0I2vGQPk1VDul5TpzGaN3nOfu83URZLJgCrX+8whS1fzMepUYrbEuIWq95esjn0gR6G4J7qlxyguAb9 admin@kubernetes' >> /root/.ssh/authorized_keys" RCFILE_03_KERNEL_DEBUG = "sysctl -w kernel.unknown_nmi_panic=1 kernel.softlockup_panic=1; modprobe netconsole netconsole=@/vmbr0,@10.9.0.15/" RCFILE_04_SYSCTL = "sysctl -w fs.file-max=20000000 fs.nr_open=20000000 net.ipv4.neigh.default.gc_thresh1=80000 net.ipv4.neigh.default.gc_thresh2=90000 net.ipv4.neigh.default.gc_thresh3=100000" RCFILE_05_FORWARD = "echo 1 > /proc/sys/net/ipv4/ip_forward" RCFILE_06_MODULES = "modprobe br_netfilter" RCFILE_07_JOIN_K8S = "kubeadm join --token 2a4576.504356e45fa3d365 10.9.0.20:6443 --discovery-token-ca-cert-hash sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBSLYRaORL2znr1V4a3rjDn3HDHn2CsvUNK1nv8 + CctoICtJOPXl6zQycI9KXNhANfJpc6iQG1ZPZUR74IiNhNIKvOpnNRPyLZ5opm01MVIDIZgi9g0DUks1g5gLV5LKzED8xYKMBmAfXMxh / nsP9KEvxGvTJB3OD + / bBxpliTl5xY3Eu41 + VmZqVOz3Yl98 + X8cZTgqx2dmsHUk7VKN9OZuCjIZL9MtJCZyOSRbjuo4HFEssotR1mvANyz + BUXkjqv2pEa0I2vGQPk1VDul5TpzGaN3nOfu83URZLJgCrX + 8whS1fzMepUYrbEuIWq95esjn0gR6G4J7qlxyguAb9 admin @ Kubernetes' >> /root/.ssh/authorized_keys" apiVersion: v1 kind: ConfigMap metadata: name: ltsp-config data: lts.conf: | [default] KEEP_SYSTEM_SERVICES = "ssh ureadahead dbus-org.freedesktop.login1 systemd-logind polkitd cgmanager ufw rpcbind nfs-kernel-server" PREINIT_00_TIME = "ln -sf /usr/share/zoneinfo/Europe/Prague /etc/localtime" PREINIT_01_FIX_HOSTNAME = "sed -i '/^127.0.0.2/d' /etc/hosts" PREINIT_02_DOCKER_OPTIONS = "sed -i 's|^ExecStart=.*|ExecStart=/usr/bin/dockerd -H fd:// --storage-driver overlay2 --iptables=false --ip-masq=false --log-driver=json-file --log-opt=max-size=10m --log-opt=max-file=5|' /etc/systemd/system/docker.service" FSTAB_01_SSH = "/dev/data/ssh /etc/ssh ext4 nofail,noatime,nodiratime 0 0" FSTAB_02_JOURNALD = "/dev/data/journal /var/log/journal ext4 nofail,noatime,nodiratime 0 0" FSTAB_03_DOCKER = "/dev/data/docker /var/lib/docker ext4 nofail,noatime,nodiratime 0 0" # Each command will stop script execution when fail RCFILE_01_SSH_SERVER = "cp /rofs/etc/ssh/*_config /etc/ssh; ssh-keygen -A" RCFILE_02_SSH_CLIENT = "mkdir -p /root/.ssh/; echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBSLYRaORL2znr1V4a3rjDn3HDHn2CsvUNK1nv8+CctoICtJOPXl6zQycI9KXNhANfJpc6iQG1ZPZUR74IiNhNIKvOpnNRPyLZ5opm01MVIDIZgi9g0DUks1g5gLV5LKzED8xYKMBmAfXMxh/nsP9KEvxGvTJB3OD+/bBxpliTl5xY3Eu41+VmZqVOz3Yl98+X8cZTgqx2dmsHUk7VKN9OZuCjIZL9MtJCZyOSRbjuo4HFEssotR1mvANyz+BUXkjqv2pEa0I2vGQPk1VDul5TpzGaN3nOfu83URZLJgCrX+8whS1fzMepUYrbEuIWq95esjn0gR6G4J7qlxyguAb9 admin@kubernetes' >> /root/.ssh/authorized_keys" RCFILE_03_KERNEL_DEBUG = "sysctl -w kernel.unknown_nmi_panic=1 kernel.softlockup_panic=1; modprobe netconsole netconsole=@/vmbr0,@10.9.0.15/" RCFILE_04_SYSCTL = "sysctl -w fs.file-max=20000000 fs.nr_open=20000000 net.ipv4.neigh.default.gc_thresh1=80000 net.ipv4.neigh.default.gc_thresh2=90000 net.ipv4.neigh.default.gc_thresh3=100000" RCFILE_05_FORWARD = "echo 1 > /proc/sys/net/ipv4/ip_forward" RCFILE_06_MODULES = "modprobe br_netfilter" RCFILE_07_JOIN_K8S = "kubeadm join --token 2a4576.504356e45fa3d365 10.9.0.20:6443 --discovery-token-ca-cert-hash sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
- KEEP_SYSTEM_SERVICES - durante a inicialização, o LTSP exclui automaticamente alguns serviços, essa variável é necessária para evitar esse comportamento para os serviços listados aqui.
- PREINIT_ * - os comandos listados aqui serão executados antes da execução do systemd (esse recurso foi adicionado pelo patch feature_preinit.diff )
- FSTAB_ * - as linhas listadas aqui serão adicionadas ao arquivo
/etc/fstab
.
Você pode perceber que eu uso a opção nofail
, ela fornece o seguinte comportamento: se a seção não existir, o download continuará sem erros. - RCFILE_ * - esses comandos serão adicionados ao arquivo
rc.local
, que será chamado pelo systemd no momento da inicialização.
Aqui, carrego os módulos necessários do kernel, executo algumas configurações do sysctl e, em seguida, execute o comando kubeadm join
, que adiciona o nó ao cluster kubernetes.
Você pode obter informações mais detalhadas sobre todas as variáveis na página de manual do lts.conf .
Agora você pode configurar seu DHCP. Basicamente, o que você precisa é especificar as opções de next-server
e filename
.
Estou usando um servidor ISC-DHCP, darei um exemplo de 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; }
Você pode começar com isso, mas quanto a mim, tenho vários servidores LTSP e, para cada nó, configuro um endereço IP estático separado e as opções necessárias usando um manual Ansible.
Tente iniciar seu primeiro nó e, se tudo foi feito corretamente, você receberá um sistema carregado. O nó também será adicionado ao cluster Kubernetes.
Agora você pode tentar fazer suas próprias alterações.
Se você precisar de algo mais, observe que o LTSP pode ser personalizado com muita facilidade para atender às suas necessidades. Sinta-se livre para procurar a fonte, onde você pode encontrar muitas respostas.
Participe do nosso canal Telegram: @ltsp_ru .