Ubuntu 26.04 LTS: guia técnico das mudanças que importam e o caminho seguro de atualização

Na madrugada de uma janela de manutenção num cluster em São Paulo, eu estava olhando um journalctl -u kubelet que insistia em cuspir mensagens de compatibilidade logo depois de uma atualização de pacote. Não era drama de laboratório. Era um host de produção, com unattended-upgrades habilitado, reboot agendado por needrestart e uma pilha inteira de workloads esperando que o kernel subisse limpo. Foi ali que ficou evidente: em Ubuntu LTS, o detalhe que separa uma migração trivial de uma semana perdida não é a versão em si, é o nível de controle que você coloca antes do reboot.

O que muda de verdade quando você sai da linha 24.04 e pisa na 26.04

Ubuntu 26.04 LTS entra na categoria de atualização que eu trato como projeto, não como rotina. Em LTS, o ganho real não é “ter novidades”. É consolidar ciclos de suporte, bater de frente com mudanças de base e encostar a infraestrutura em uma plataforma que conversa melhor com toolchains recentes, kernels mais novos, systemd mais maduro e cadeias de segurança menos improvisadas. Para quem mantém VMs, bare metal, clusters Kubernetes, nós de CI ou bastions, a pergunta certa não é “quais features novas apareceram?”, e sim “quais pontos de integração do meu ambiente vão sentir a troca de ABI, do kernel, do userspace e dos defaults?”.

Eu não compro atualização de LTS por marketing. Compro por previsibilidade operacional. E justamente por isso o que importa em 26.04 é o conjunto de mudanças na base: kernel, systemd, OpenSSH, toolchain, stack de rede, políticas de segurança e a forma como os componentes de instalação e gerenciamento foram refinados. Em servidores, pequenas mudanças em systemd-networkd, netplan, iptables/nft, AppArmor e no comportamento de pacotes críticos importam mais do que qualquer interface bonita no desktop. Para quem opera com automação, o que conta é saber o que quebrará seu Terraform, seu Ansible e seu pipeline de imagem.

Se você trabalha com image baking, o primeiro impacto está no pipeline. Nova LTS significa novo cloud-init, novo kernel baseline, novas versões de Python no sistema, pacotes atualizados e, muitas vezes, mudanças de comportamento em módulos do Ansible que dependem do Python do host. Isso não é detalhe. Se a sua imagem Packer depende de python3-apt, debian-archive-keyring, ca-certificates e repositórios de terceiros, você precisa testar o bootstrap desde o estágio mais cru possível.

Onde o ganho aparece no dia a dia: kernel, systemd, rede e segurança

Em LTS, eu costumo separar os efeitos em quatro camadas. A primeira é kernel e drivers: melhor suporte a hardware recente, correções em cgroup, scheduler, filesystem e rede. A segunda é systemd, que mexe com boot, units, timers, journald e integração com o resto do userspace. A terceira é a pilha de rede, onde Netplan, NetworkManager, systemd-networkd e nftables precisam continuar previsíveis sob automação. A quarta é segurança: AppArmor, OpenSSL, OpenSSH, cryptsetup, políticas de atualização e hardening padrão.

Se o seu ambiente é de servidor, kernel novo normalmente significa menos dor com NICs modernas, NVMe, controladoras virtuais e workloads que dependem de cgroups mais consistentes. Em container hosts, eu olho especialmente para comportamento de cgroup v2, limites de memória, pressão de IO e compatibilidade com runtimes. Se o ambiente roda Docker, containerd ou CRI-O, tudo que encosta em /sys/fs/cgroup precisa ser validado com carga real. Não adianta só subir um container Alpine e declarar vitória.

O systemd em uma LTS nova quase sempre traz ajustes que afetam boot time, gestão de units e limpeza de serviços. Em servidores com dezenas de serviços customizados, eu reviso sempre:

  • Restart= e RestartSec= em units críticas
  • After= e Requires= quando há dependências de rede e storage
  • StartLimitBurst= para evitar loops agressivos
  • TimeoutStartSec= em serviços que dependem de DNS, mount remoto ou endpoints externos

Se você nunca revisou uma unit file com atenção, a atualização é um ótimo motivo. Um systemd-analyze blame e um systemd-analyze critical-chain mostram mais do que meia dúzia de notas de release.

Comandos que eu rodaria antes de qualquer reboot

uname -a
lsb_release -a
systemd-analyze
systemd-analyze blame
systemctl list-unit-files --state=enabled
apt-mark showhold
apt list --upgradable
journalctl -p 3 -xb

Esse bloco já entrega metade dos problemas. apt-mark showhold é especialmente chato e especialmente útil; pacotes travados por hold podem impedir a resolução correta de dependências durante um do-release-upgrade. Em ambientes com compliance, isso aparece o tempo todo porque alguém congelou linux-image-generic, openssl ou docker-ce em uma janela anterior e esqueceu de documentar.

Antes de tocar no release-upgrade: inventário, backup e congelamento operacional

Eu trato atualização de LTS como mudança controlada. Não começo no comando de upgrade. Começo com inventário. Preciso saber exatamente o que existe naquele host: serviços ativos, pacotes fora do repositório padrão, mounts, volumes LVM, VPNs, agentes de monitoramento, exporters, drivers proprietários, regras de firewall e qualquer coisa que dependa de binários compilados localmente. Se você tem um servidor “só de app”, quase sempre existe um plugin, um binário velho ou um script de init que ninguém lembra que está lá.

Meu checklist operacional é seco:

dpkg -l | awk '/^ii/ {print $2, $3}' > /root/pkg-inventory.txt
find /etc -maxdepth 2 -type f | sort > /root/etc-inventory.txt
ss -tulpn > /root/lista-portas.txt
mount > /root/mounts.txt
ip a > /root/ip-a.txt
ip r > /root/ip-r.txt
crontab -l > /root/crontab-root.txt
systemctl list-units --type=service --state=running > /root/services-running.txt

Backup não é só snapshot de VM. Snapshot de VM ajuda, mas eu prefiro ter duas camadas: snapshot reversível e backup coerente do que realmente importa. Se há banco local, eu paro o serviço, gero dump consistente ou uso snapshot do volume com fsfreeze quando o storage permite. Em ZFS, o caminho é um; em LVM, outro. Em cloud, o snapshot do disco pode ser suficiente para rollback curto, mas só se você já testou restauração. Sem isso, snapshot é esperança com interface gráfica.

Para hosts críticos, eu gosto de usar um playbook de pré-check. Algo assim:

- name: Pré-check de upgrade Ubuntu
 hosts: all
 become: true
 tasks:
 - name: Coletar versão do sistema
 command: lsb_release -a
 register: lsb
 changed_when: false

 - name: Ver holds em pacotes
 shell: apt-mark showhold
 register: holds
 changed_when: false

 - name: Falhar se houver pacotes travados
 fail:
 msg: "Pacotes em hold encontrados: {{ holds.stdout_lines }}"
 when: holds.stdout_lines | length > 0

 - name: Verificar espaço em disco
 shell: df -h / /var /boot
 register: dfout
 changed_when: false

 - debug:
 var: dfout.stdout_lines

Esse tipo de automação evita o upgrade “na mão” que vira improviso. E improviso em LTS normalmente custa mais do que a janela de manutenção inteira.

O caminho correto do 24.04 para o 26.04: paciência, repositórios limpos e nada de atalho estranho

Para a atualização em si, eu sigo o fluxo suportado pelo Ubuntu. Nada de misturar apt full-upgrade com troca manual de codename no sources.list enquanto o sistema ainda está no meio de uma transição. O caminho certo passa por sistema atualizado, repositórios de terceiros revisados e o utilitário de release upgrade quando a versão estiver disponível. Em produção, eu espero a fumaça baixar. Em homelab, eu testo antes. Em ambos, eu nunca pulo o inventário dos PPAs.

O baseline antes do upgrade precisa estar limpo:

sudo apt update
sudo apt full-upgrade -y
sudo apt autoremove --purge -y
sudo reboot

Depois do reboot, eu confirmo kernel, serviços e ausência de corrupção de pacotes:

sudo dpkg --audit
sudo apt -f install
sudo journalctl -p 3 -b

Se existir PPA, repositório vendor ou pacote de terceiros, eu desativo antes da transição. Isso é obrigação. Um repositório que ainda não publicou build para a release nova é a forma mais rápida de fazer apt travar em dependências quebradas. O mínimo aceitável é documentar todos eles em um arquivo de gestão de configuração, ou pelo menos com uma tarefa Ansible que os remova temporariamente.

Exemplo de limpeza de PPAs antes da migração

grep -R "^deb " /etc/apt/sources.list /etc/apt/sources.list.d/*.list
sudo add-apt-repository --remove ppa:algum/ppa
sudo rm -f /etc/apt/sources.list.d/repositorio-externo.list
sudo apt update

Quando a nova versão estiver liberada para seu ciclo, o comando padrão entra em cena:

sudo do-release-upgrade

Em servidor remoto, eu sempre faço isso dentro de tmux ou screen. Sem exceção. E com SSH persistente, ServerAliveInterval ajustado no cliente. Perder a sessão no meio do upgrade é o tipo de incidente evitável que consome uma madrugada inteira.

Se você administra muitas máquinas, a abordagem correta não é logar em cada uma e apertar Enter. É transformar o processo em rollout controlado. Exemplo simples com Ansible:

- name: Atualizar Ubuntu para nova LTS
 hosts: ubuntu_servers
 become: true
 serial: 1
 tasks:
 - name: Garantir sistema atualizado
 apt:
 update_cache: true
 upgrade: dist

 - name: Remover pacotes obsoletos
 apt:
 autoremove: true
 purge: true

 - name: Executar upgrade de release
 command: do-release-upgrade -f DistUpgradeViewNonInteractive
 register: upgrade_out
 async: 14400
 poll: 30

 - debug:
 var: upgrade_out.stdout_lines

Eu usaria isso só em ambiente bem conhecido, porque do-release-upgrade não é um módulo idempotente perfeito e exige supervisão. Mas a estrutura ajuda. Em ambiente maior, prefiro orquestrar por grupos, com drenagem prévia de nós em load balancer, checks de saúde e reanexação só depois da validação.

O que eu validaria depois do boot novo, sem confiar em feeling

Boot subiu? Ótimo. Agora começa o trabalho de verdade. Eu nunca aceito “subiu no console” como sinal de sucesso. Quero SSH, kernel correto, rede íntegra, serviços críticos rodando e logs sem erro cinzento escondido. O primeiro comando costuma ser:

lsb_release -a
uname -r
systemctl --failed
journalctl -b -p 3

Em seguida, eu valido DNS, rotas, resolução reversa se a aplicação depende disso, conectividade com storage e health checks. Se houver Kubernetes no host, eu verifico kubelet, runtime, cgroup e estado dos pods. Se houver banco local, eu faço um teste de conexão e uma consulta mínima. Se houver NFS, SMB, iSCSI ou CephFS, eu monto e testo leitura e escrita. O objetivo é detectar regressão que o boot não acusa.

Um script de pós-check ajuda bastante:

#!/usr/bin/env bash
set -euo pipefail

echo "Kernel: $(uname -r)"
echo "OS: $(lsb_release -ds)"

echo "=== Failed units ==="
systemctl --failed || true

echo "=== Listening ports ==="
ss -tulpn

echo "=== Disk usage ==="
df -hT

echo "=== Recent errors ==="
journalctl -b -p 3 --no-pager | tail -n 100

Eu gosto também de validar integridade de pacotes instalados, principalmente em hosts com manutenção antiga:

sudo debsums -s

Se debsums apontar alterações em binários críticos, eu investigo antes de culpar a atualização. Em várias operações, o problema já existia e só apareceu porque o upgrade obrigou o sistema a revelar a sujeira.

Automação de imagem: Packer, cloud-init e golden images alinhadas com a nova base

Se você mantém imagens padrão para OpenStack, Proxmox, VMware, AWS, Azure ou bare metal, a migração para 26.04 é uma oportunidade excelente para ajustar o pipeline. Eu prefiro gerar nova imagem dourada em vez de atualizar VMs uma a uma quando a frota é grande e homogênea. É mais limpo. Menos drift. Menos exceção.

Um fluxo decente combina Packer, cloud-init e provisionamento por Ansible. O Packer cria a imagem base; o cloud-init trata bootstrap; o Ansible aplica configuração de aplicação e hardening. Um exemplo minimalista de Packer com Ubuntu cloud image pode ficar assim:

{
 "builders": [
 {
 "type": "qemu",
 "iso_url": "https://cloud-images.ubuntu.com/releases/26.04/release/ubuntu-26.04-server-cloudimg-amd64.img",
 "output_directory": "output-ubuntu-2604",
 "format": "qcow2",
 "accelerator": "kvm",
 "disk_interface": "virtio"
 }
 ],
 "provisioners": [
 {
 "type": "shell",
 "inline": [
 "apt-get update",
 "apt-get -y install qemu-guest-agent cloud-init"
 ]
 }
 ]
}

O cloud-init precisa ser tratado como código. Não como arquivo mágico que alguém copia de um host antigo. Um exemplo pragmático:

#cloud-config
package_update: true
package_upgrade: true
packages:
 - qemu-guest-agent
 - curl
 - vim
 - chrony
runcmd:
 - systemctl enable --now qemu-guest-agent
 - systemctl enable --now chrony

Quando você sobe a nova imagem, valida o comportamento com testes automatizados. Eu gosto de usar Molecule para roles do Ansible e um conjunto curto de smoke tests no CI. Exemplo de pipeline GitLab CI:

stages:
 - lint
 - test
 - bake

ansible-lint:
 stage: lint
 image: python:3.12
 script:
 - pip install ansible-lint
 - ansible-lint roles/

molecule:
 stage: test
 image: python:3.12
 script:
 - pip install ansible molecule molecule-plugins[lint]
 - molecule test

Esse ciclo evita que a nova base Ubuntu entre na infraestrutura como se fosse um salto no escuro. Não é salto. É engenharia operacional com rastreabilidade.

Docker, Kubernetes e a fricção que quase ninguém contabiliza

Em hosts que servem containers, a atualização do sistema operacional afeta o que está abaixo da runtime. E isso repercute em imagens, volumes, cgroups e observabilidade. Quando a base muda, eu testo pelo menos quatro coisas: lifecycle de containers, resolução de DNS dentro do namespace, acesso a volumes e limites de recursos. Se você usa Docker direto no host, também precisa revisar o daemon e a configuração de storage driver.

Um /etc/docker/daemon.json saudável para ambientes previsíveis pode ser algo assim:

{
 "log-driver": "json-file",
 "log-opts": {
 "max-size": "10m",
 "max-file": "3"
 },
 "storage-driver": "overlay2",
 "exec-opts": ["native.cgroupdriver=systemd"]
}

Depois de atualizar a LTS, eu reinicio a runtime e comparo o comportamento com docker info ou crictl info. Se a plataforma usa Kubernetes, o cuidado é maior. Um upgrade do host altera o suporte de kernel para a runtime e para o kubelet. Eu costumo drenar o nó, aplicar a atualização, validar o boot, depois liberar de volta ao cluster.

kubectl drain node-01 --ignore-daemonsets --delete-emptydir-data
ssh node-01 'sudo do-release-upgrade'
kubectl uncordon node-01
kubectl get nodes -o wide

Esse fluxo é chato. E é exatamente por isso que funciona. Atualização de LTS em nó de cluster não precisa ser empolgante; precisa ser previsível.

Hardening pós-migração: onde eu mexo antes de devolver o host para a produção

Depois que o sistema sobe em 26.04, eu não devolvo o host para produção sem revisar baseline de segurança. Isso inclui SSH, sudo, firewall, auditoria e updates automáticos. Em LTS, gosto de deixar o host com comportamento explícito. Nada de default ambíguo.

Um sshd_config enxuto, mais restritivo do que o padrão, geralmente começa assim:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
UsePAM yes
X11Forwarding no
AllowTcpForwarding yes
ClientAliveInterval 300
ClientAliveCountMax 2

Em firewall, eu prefiro nftables ou UFW dependendo do perfil do time. Para infra pequena, UFW resolve. Para infra maior e mais integrada, nftables ganha por clareza e controle. Exemplo simples em nftables:

table inet filter {
 chain input {
 type filter hook input priority 0;
 policy drop;
 ct state established,related accept
 iif lo accept
 tcp dport { 22, 80, 443 } accept
 ip protocol icmp accept
 }
}

Depois, eu reviso unattended-upgrades e janela de reboot. Um sistema LTS sem política de patching vira problema de governança em poucas semanas. Configuração típica:

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:30";

Se isso não fizer sentido para o seu ambiente, desabilite e gerencie reboot por orquestração. O que não dá é fingir que manutenção acontece sozinha.

Os pontos que mais quebram e que eu checaria primeiro

Alguns problemas aparecem repetidamente em upgrade grande de Ubuntu, e eu já entro olhando neles:

  • pacotes de terceiros sem suporte para a nova release;
  • unidades systemd customizadas com dependência fraca;
  • configurações antigas de rede em /etc/network/interfaces convivendo mal com Netplan;
  • agentes de monitoramento e backup compilados para versões antigas de glibc;
  • módulos de kernel fora da árvore para hardware específico;
  • scripts que assumem Python antigo no sistema.

O item de Python merece atenção especial. Muitas automações ainda assumem que o /usr/bin/python existe ou que a versão do sistema bate com o que o playbook espera. Hoje, eu sempre deixo o inventário Ansible explícito:

all:
 vars:
 ansible_python_interpreter: /usr/bin/python3

Também reviso dependências de pip instaladas no sistema. Em servidores, eu prefiro venv ou container para apps Python. Misturar app crítico com pip install no Python do sistema continua sendo um hábito caro.

Estratégia de rollback que não depende de sorte

Rollback de release upgrade raramente é elegante. Por isso ele precisa existir antes da mudança. Se a máquina está em VM, snapshot consistente é a saída mais limpa. Se está em bare metal, eu quero backup de dados e procedimento claro de reinstalação em image-based recovery. Em ambientes com IaC, o rollback ideal é recriar a máquina em 24.04 a partir do estado anterior, restaurar dados e recolocar atrás do balanceador. Isso costuma ser mais confiável do que tentar “desfazer” uma atualização que já mexeu em pacotes base, kernel e dependências.

Eu documentaria o rollback com uma runbook objetiva:

# 1. Remover nó do balanceador
# 2. Restaurar snapshot ou recriar VM
# 3. Reaplicar configuração via Ansible
# 4. Restaurar dados a partir de backup
# 5. Validar health checks

Sem esse caminho definido, qualquer falha de upgrade vira incidente de improviso. E improviso em produção sempre encontra o pior horário possível.

O que eu faria no primeiro dia com Ubuntu 26.04 LTS em produção

Eu começaria pelos hosts menos críticos, de preferência um ambiente idêntico ao real. Rodaria os testes de boot, rede, observabilidade e aplicação. Depois compararia métricas antes e depois: tempo de boot, uso de memória do sistema, latência de deploy, consumo de CPU do agente de monitoramento, falhas de serviço, eventos de journald. Se o ambiente depende de SLAs apertados, eu colocaria um canário com tráfego limitado e observação forte.

Depois disso, eu congelaria a imagem nova como padrão e reduziria a diversidade do parque. Menos variantes, menos manutenção. Se você me perguntar qual é o ganho mais concreto de uma LTS nova bem adotada, eu respondo sem hesitar: menos atrito entre o host e a automação. Quando o host, o pipeline e o runtime passam a falar a mesma língua, a operação fica mais silenciosa. E operação silenciosa é boa operação.

Se você quiser fazer isso direito, trate o upgrade como uma mudança de infraestrutura, não como um clique administrativo. Atualize com inventário, valide com automação, e só depois solte o host de volta para o tráfego. É esse rigor que separa uma LTS usada de uma LTS dominada.