Entendendo Unidades do Systemd: Um Mergulho Profundo na Configuração de Serviços

Aprenda como funcionam as unidades de serviço do systemd, incluindo Unit, Service, Install, sobrescritas, reinicializações e logs.

Entendendo Unidades do Systemd: Um Mergulho Profundo na Configuração de Serviços

Os arquivos de unidade do systemd são pequenos arquivos de texto que decidem como os serviços iniciam, de quais dependem, como qual usuário executam e o que acontece quando falham. Se você já se perguntou por que systemctl restart myapp.service funciona para um aplicativo, mas não para outro, a resposta geralmente está no arquivo de unidade.

Este guia foca nas unidades .service porque são as que administradores e desenvolvedores mais editam. O mesmo sistema também gerencia sockets, timers, montagens, dispositivos, caminhos e targets, mas os arquivos de serviço são onde a maioria dos erros operacionais aparece.

O que são Arquivos de Unidade do Systemd?

Arquivos de unidade do systemd são arquivos de texto simples que contêm diretivas de configuração para uma unidade específica. Uma unidade representa um recurso gerenciado pelo systemd. O tipo mais comum é a unidade de serviço, que define como iniciar, parar, reiniciar e gerenciar um processo em segundo plano ou aplicativo.

Os arquivos de unidade são organizados em seções, cada uma denotada por colchetes ([]). As seções mais importantes para unidades de serviço são:

  • [Unit]: Contém metadados sobre a unidade, dependências e ordenação.
  • [Service]: Define o comportamento do serviço em si, incluindo como executá-lo.
  • [Install]: Especifica como a unidade deve ser habilitada ou desabilitada, tipicamente vinculando-a a unidades de destino.

O systemd procura por arquivos de unidade em vários diretórios padrão, sendo os mais comuns:

  • /etc/systemd/system/: Para unidades configuradas localmente, sobrescrevendo as padrão.
  • /usr/lib/systemd/system/: Para unidades instaladas por pacotes em muitas distribuições.
  • /lib/systemd/system/: Usado por alguns sistemas da família Debian para unidades fornecidas por pacotes.

Quando precisar inspecionar uma unidade, evite adivinhar o caminho. Use:

systemctl cat nginx.service
systemctl show -p FragmentPath nginx.service

systemctl cat é especialmente útil porque mostra a unidade base mais quaisquer sobrescritas drop-in. Essa é a versão que o systemd está realmente usando.

Anatomia de um Arquivo de Unidade .service

Vamos detalhar um arquivo de unidade .service típico para entender seus componentes.

A Seção [Unit]

Esta seção fornece informações descritivas e define os relacionamentos entre as unidades.

  • Description=: Uma descrição legível por humanos do serviço.
  • Documentation=: URLs ou caminhos para documentação do serviço.
  • After=: Especifica que esta unidade deve iniciar após as unidades listadas terem terminado de iniciar.
  • Requires=: Semelhante a After=, mas também torna as unidades listadas obrigatórias. Se uma unidade requerida falhar ao iniciar, esta unidade também falhará.
  • Wants=: Uma forma mais fraca de dependência. Esta unidade tentará iniciar suas unidades desejadas, mas a falha delas não impedirá esta unidade de iniciar.
  • Conflicts=: Especifica unidades que não podem ser executadas concorrentemente com esta unidade.

Exemplo de seção [Unit]:

[Unit]
Description=Meu Servidor Web Personalizado
Documentation=https://exemplo.com/docs/meu-servidor-web
After=network.target

Isso indica que nosso servidor web personalizado deve iniciar após a rede estar disponível.

Uma armadilha comum: After= controla a ordem, não o requisito. Se você escrever After=postgresql.service, o systemd inicia seu serviço após o PostgreSQL quando ambos fazem parte da transação, mas não puxa automaticamente o PostgreSQL. Se seu aplicativo realmente precisa que o PostgreSQL seja iniciado pela mesma transação, use Wants=postgresql.service ou, para uma dependência forte, Requires=postgresql.service também.

Mesmo assim, dependências não são verificações de saúde. After=network.target não garante que o DNS funcione, que uma API remota esteja acessível ou que um banco de dados esteja aceitando conexões. Seu aplicativo ainda precisa de um comportamento de repetição sensato.

A Seção [Service]

É aqui que reside a lógica central para executar o serviço.

  • Type=: Define o tipo de inicialização do processo. Tipos comuns incluem:
    • simple (padrão): O processo principal é aquele iniciado por ExecStart=. O systemd considera o serviço iniciado imediatamente após o processo ExecStart= ser bifurcado.
    • forking: Usado para daemons tradicionais que bifurcam um processo filho e saem. O systemd espera o processo pai sair.
    • oneshot: Para tarefas que executam um único comando e depois saem.
    • notify: O serviço envia uma notificação ao systemd quando terminou de iniciar.
    • dbus: Para serviços que adquirem um nome D-Bus.
  • ExecStart=: O comando a ser executado para iniciar o serviço.
  • ExecStop=: O comando a ser executado para parar o serviço.
  • ExecReload=: O comando a ser executado para recarregar a configuração do serviço sem reiniciá-lo.
  • Restart=: Define quando o serviço deve ser reiniciado. As opções incluem no (padrão), on-success, on-failure, on-abnormal, on-watchdog, on-abort e always.
  • RestartSec=: O tempo de espera antes de reiniciar o serviço.
  • User= / Group=: O usuário e grupo sob os quais o serviço deve ser executado.
  • WorkingDirectory=: O diretório de trabalho para os processos executados.
  • Environment= / EnvironmentFile=: Define variáveis de ambiente para o serviço.

Exemplo de seção [Service]:

[Service]
Type=simple
ExecStart=/usr/local/bin/meu-servidor-web --config /etc/meu-servidor-web.conf
User=www-data
Group=www-data
Restart=on-failure
RestartSec=5

Esta configuração inicia nosso servidor web, executa-o como usuário e grupo www-data e o reinicia automaticamente se falhar, com um atraso de 5 segundos.

Type= merece cuidado extra. Muitas unidades quebradas usam Type=forking porque um script init antigo usava modo daemon. Para um aplicativo moderno que permanece em primeiro plano, Type=simple geralmente está correto. Se seu processo bifurca para segundo plano, mas o systemd não é informado sobre como identificar o processo principal real, o relatório de status e as reinicializações podem se tornar enganosos.

Para um trabalho único, use Type=oneshot e frequentemente RemainAfterExit=yes se a ação concluída deve contar como ativa. Por exemplo, uma unidade que prepara uma regra de firewall ou monta um recurso especial pode sair com sucesso, mas ainda representar um estado que você se importa.

A Seção [Install]

Esta seção é usada ao habilitar ou desabilitar uma unidade. Ela define como a unidade se integra com as unidades de destino do systemd.

  • WantedBy=: Especifica o(s) destino(s) que devem "querer" esta unidade quando ela for habilitada. Para serviços que devem iniciar na inicialização, multi-user.target é comumente usado.

Exemplo de seção [Install]:

[Install]
WantedBy=multi-user.target

Quando você executa systemctl enable meu-servico-personalizado.service, o systemd cria um link simbólico de /etc/systemd/system/multi-user.target.wants/ para seu arquivo de serviço, garantindo que ele inicie quando o sistema atingir o runlevel multiusuário.

Se uma unidade não tem seção [Install], ela ainda pode ser perfeitamente válida. Apenas não pode ser habilitada diretamente com systemctl enable a menos que outro mecanismo de instalação exista. Algumas unidades são destinadas a serem puxadas por dependências, sockets, timers ou destinos, em vez de serem habilitadas manualmente.

Criando e Gerenciando Unidades de Serviço Personalizadas

Vamos percorrer o processo de criação de uma unidade de serviço personalizada.

Passo 1: Criar o Arquivo de Unidade

Crie um novo arquivo em /etc/systemd/system/ com extensão .service. Para nosso exemplo, vamos criar /etc/systemd/system/meu-app.service.

[Unit]
Description=Serviço de Aplicativo Personalizado
After=network.target

[Service]
Type=simple
ExecStart=/opt/meu-app/bin/executar-app --porta 8080
User=appuser
Group=appgroup
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Considerações Importantes:

  • Certifique-se de que o comando ExecStart aponte para um script executável ou binário que seja acessível e tenha permissões de execução.
  • Crie o User e Group especificados se eles não existirem (sudo useradd -r -s /bin/false appuser, sudo groupadd appgroup, sudo usermod -a -G appgroup appuser).
  • Certifique-se de que o aplicativo possa ser iniciado e parado corretamente usando os comandos especificados.

Antes de colocar um comando em ExecStart=, execute-o manualmente como o mesmo usuário, se possível:

sudo -u appuser /opt/meu-app/bin/executar-app --porta 8080

Isso captura bits de execução ausentes, diretórios faltantes, caminhos relativos ruins e problemas de permissão antes que o systemd esteja envolvido. Depois que funcionar manualmente, mova-o para a unidade e deixe o systemd cuidar da supervisão.

Passo 2: Recarregar a Configuração do Systemd

Após criar ou modificar um arquivo de unidade, você deve informar ao systemd para recarregar sua configuração.

sudo systemctl daemon-reload

Este comando verifica se há arquivos de unidade novos ou alterados e atualiza o estado interno do systemd.

Passo 3: Habilitar e Iniciar o Serviço

Para iniciar o serviço imediatamente e configurá-lo para iniciar na inicialização:

sudo systemctl enable meu-app.service  # Cria links simbólicos para inicialização
sudo systemctl start meu-app.service   # Inicia o serviço agora

Passo 4: Gerenciar o Serviço

Use comandos systemctl para gerenciar seu serviço:

  • Verificar status:

    sudo systemctl status meu-app.service
    

    Isso mostrará se o serviço está ativo, seu ID de processo, entradas de log recentes e mais.

  • Parar o serviço:

    sudo systemctl stop meu-app.service
    
  • Reiniciar o serviço:

    sudo systemctl restart meu-app.service
    
  • Recarregar o serviço (se ExecReload= estiver definido):

    sudo systemctl reload meu-app.service
    
  • Desabilitar o serviço (impedir de iniciar na inicialização):

    sudo systemctl disable meu-app.service
    

Passo 5: Visualizar Logs com journalctl

O systemd se integra fortemente com o journald para registro de logs. Você pode visualizar logs do seu serviço usando journalctl:

  • Visualizar logs de um serviço específico:

    sudo journalctl -u meu-app.service
    
  • Seguir logs em tempo real:

    sudo journalctl -f -u meu-app.service
    
  • Visualizar logs desde a última inicialização:

    sudo journalctl -b -u meu-app.service
    

Melhores Práticas e Dicas

  • Use Type=notify para aplicativos modernos: Se seu aplicativo suportar, Type=notify fornece melhor integração com o systemd, permitindo rastrear com precisão a prontidão do serviço.
  • Execute serviços como usuários não root: Sempre especifique User= e Group= na seção [Service] para minimizar riscos de segurança.
  • Defina dependências cuidadosamente: Use After=, Requires= e Wants= para garantir que os serviços iniciem na ordem correta e que dependências críticas sejam atendidas.
  • Aproveite Restart=: Configure políticas de reinicialização apropriadas para garantir a disponibilidade do serviço.
  • Mantenha arquivos de unidade simples: Para sequências de inicialização complexas, considere usar scripts wrapper invocados por ExecStart= em vez de comandos complexos diretamente no arquivo de unidade.
  • Use systemctl cat <unidade>: Para visualizar o conteúdo completo de um arquivo de unidade como o systemd o vê, incluindo quaisquer sobrescritas.
  • Use systemctl edit <unidade>: Este comando abre um editor para criar um arquivo de sobrescrita para uma unidade existente, que é uma maneira mais limpa de modificar arquivos de unidade padrão do que editá-los diretamente.

Editando Unidades Existentes com Segurança

Não edite unidades pertencentes a pacotes em /usr/lib/systemd/system/ ou /lib/systemd/system/ a menos que esteja depurando uma máquina descartável. As atualizações de pacotes podem substituir esses arquivos. Use uma sobrescrita:

sudo systemctl edit nginx.service

Isso cria um drop-in em /etc/systemd/system/nginx.service.d/. Por exemplo, para adicionar uma política de reinicialização:

[Service]
Restart=on-failure
RestartSec=5s

Algumas diretivas podem ser especificadas mais de uma vez. Outras precisam ser limpas antes da substituição. ExecStart= é o exemplo clássico:

[Service]
ExecStart=
ExecStart=/usr/local/bin/meu-wrapper-nginx

A linha ExecStart= em branco redefine o valor anterior. Sem ela, o systemd pode rejeitar a unidade ou manter mais comandos do que você pretendia.

Após qualquer alteração de unidade ou drop-in, use o mesmo loop de revisão:

sudo systemctl daemon-reload
systemctl cat meu-app.service
sudo systemctl restart meu-app.service
journalctl -u meu-app.service -n 50 --no-pager

Arquivos de unidade não são difíceis depois que você separa os três trabalhos: [Unit] descreve relacionamentos, [Service] descreve o comportamento do processo e [Install] descreve a habilitação. A maior parte da depuração no mundo real é apenas descobrir qual desses trabalhos foi configurado com a suposição errada.

Um Exemplo Realista de Arquivo de Serviço

Aqui está um exemplo pequeno, mas realista, de um serviço para um aplicativo web Python:

[Unit]
Description=API de Inventário
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=inventory
Group=inventory
WorkingDirectory=/srv/inventory-api
EnvironmentFile=/etc/inventory-api/env
ExecStart=/srv/inventory-api/.venv/bin/gunicorn app:app --bind 127.0.0.1:9000
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

Há várias decisões sutis nesse arquivo. O serviço é executado como inventory, não root. O comando usa um caminho absoluto para o gunicorn do ambiente virtual, então não depende do PATH de um shell interativo. O aplicativo vincula-se a localhost porque um proxy reverso o exporá publicamente. O arquivo de ambiente fica fora da unidade para que a implantação possa atualizar a configuração sem reescrever metadados de serviço pertencentes ao pacote.

As linhas de dependência são intencionalmente modestas. After=postgresql.service controla a ordem se o PostgreSQL fizer parte da mesma transação de inicialização. Isso não prova que o banco de dados está pronto para conexões e não substitui a lógica de repetição do aplicativo. network-online.target pode ajudar em sistemas que implementam corretamente a prontidão de rede, mas não é uma garantia universal de que toda dependência remota está acessível.

Se este serviço falhar, as primeiras verificações são previsíveis:

systemctl status inventory-api.service
journalctl -u inventory-api.service -b --no-pager
systemctl cat inventory-api.service
sudo -u inventory /srv/inventory-api/.venv/bin/gunicorn app:app --bind 127.0.0.1:9000

O último comando não é algo que você deixa rodando em produção. É uma verificação de diagnóstico que pergunta: "O usuário configurado pode executar este comando?" Se ele não puder importar o aplicativo, ler o arquivo de ambiente ou escrever em seu diretório de log, o systemd não vai corrigir isso para você.

Diretivas de Recurso e Segurança que Você Verá Frequentemente

Muitas unidades de produção incluem hardening ou controles de recursos. Alguns exemplos comuns:

[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
MemoryMax=512M
CPUQuota=80%

Essas diretivas podem ser muito úteis, mas também podem quebrar suposições. PrivateTmp=true dá ao serviço um /tmp privado, então outro processo pode não ver arquivos que ele escreve lá. ProtectHome=true pode bloquear o acesso a /home, /root e /run/user. ProtectSystem=full torna grande parte do sistema somente leitura do ponto de vista do serviço. Se um aplicativo de repente não consegue escrever onde costumava, inspecione as configurações de hardening antes de culpar o aplicativo.

Os limites de recursos têm a mesma troca. MemoryMax= pode impedir que um serviço consuma toda a máquina, mas se o valor for muito baixo, o serviço pode ser morto sob carga normal. Verifique o journal em busca de mensagens de falta de memória e compare o limite com o uso real antes de aumentá-lo ou removê-lo.

Os Comandos de Depuração Mais Úteis

Mantenha estes por perto ao trabalhar com unidades de serviço:

systemctl status meu-app.service
systemctl cat meu-app.service
systemctl show meu-app.service
systemd-analyze verify /etc/systemd/system/meu-app.service
journalctl -u meu-app.service -b --no-pager

systemctl show é verboso, mas expõe as propriedades que o systemd calculou após analisar a unidade. Isso pode revelar um valor surpreendente herdado de um padrão, um drop-in ou uma diretiva de reset. systemd-analyze verify captura alguns erros de sintaxe e dependência antes de você reiniciar um serviço. Não substitui o teste do aplicativo, mas captura erros suficientes para valer a pena executar.