Gerenciamento de Configuração 101: Escrevendo Manifests do Puppet

Introdução

Em poucas palavras, o gerenciamento de configuração de servidor (também conhecido como IT Automation) é uma solução para transformar a sua administração de infraestrutura em uma base de código, descrevendo todos os processos necessários para fazer o deploy de um servidor em um conjunto de scripts de provisionamento que podem ser versionados e facilmente reutilizados. Isso pode melhorar muito a integridade de qualquer infraestrutura de servidor ao longo do tempo.

Em um guia anterior, falamos sobre os principais benefícios da implementação de uma estratégia de gerenciamento de configuração para a infraestrutura de servidor, como as ferramentas de gerenciamento de configuração funcionam e o que essas ferramentas têm em comum normalmente.

Esta parte da série o guiará pelo processo de automatização do provisionamento de servidores usando o Puppet, uma ferramenta popular de gerenciamento de configuração capaz de gerenciar infraestruturas complexas de maneira transparente, usando um servidor mestre ou master para orquestrar a configuração dos nodes. Vamos nos concentrar na terminologia da linguagem, sintaxe e recursos necessários para criar um exemplo simplificado para automatizar completamente o deployment de um servidor web Ubuntu 18.04 usando o Apache.

Esta é a lista de passos que precisamos automatizar para alcançar nosso objetivo:

  1. Atualizar o cache do apt
  2. Instalar o Apache
  3. Criar um diretório raiz de documentos personalizado (document root)
  4. Colocar um arquivo index.html no document root personalizado
  5. Aplicar um modelo para configurar nosso virtual host personalizado
  6. Reiniciar o Apache

Começaremos examinando a terminologia usada pelo Puppet, seguida de uma visão geral dos principais recursos da linguagem que podem ser usados para escrever manifests. No final deste guia, compartilharemos o exemplo completo para que você possa experimentar sozinho.

Nota: este guia pretende apresentar a linguagem do Puppet e como escrever manifests para automatizar o provisionamento de servidor. Para uma visão mais introdutória do Puppet, incluindo as etapas necessárias para instalar e começar com esta ferramenta, consulte a documentação oficial do Puppet.

Começando

Antes de podermos avançar para uma visão mais prática do Puppet, é importante nos familiarizarmos com importantes terminologias e conceitos introduzidos por esta ferramenta.

Termos do Puppet

  • Puppet Master: o servidor principal que controla a configuração nos nodes
  • Puppet Agent Node: um node controlado por um Puppet Master
  • Manifest: um arquivo que contém um conjunto de instruções a serem executadas
  • Resource (recurso): uma parte do código que declara um elemento do sistema e como seu estado deve ser alterado. Por exemplo, para instalar um pacote, precisamos definir um recurso package e garantir que seu estado esteja definido como installed
  • Module: uma coleção de manifests e outros arquivos relacionados organizados de uma forma predefinida para facilitar o compartilhamento e a reutilização de partes de um provisionamento
  • Class: Assim como nas linguagens de programação regulares, as classes são usadas no Puppet para organizar melhor o provisionamento e facilitar a reutilização de partes do código
  • Facts: variáveis globais que contêm informações sobre o sistema, como interfaces de rede e sistema operacional
  • Services: usado para disparar alterações de status do serviço, como reiniciar ou parar um serviço

O provisionamento no Puppet é escrito usando uma DSL (linguagem específica de domínio) personalizada baseada em Ruby.

Recursos

Com o Puppet, as tarefas ou etapas são definidas declarando recursos. Os recursos podem representar pacotes, arquivos, serviços, usuários e comandos. Eles podem ter um estado, que acionará uma alteração no sistema caso o estado de um recurso declarado seja diferente do que está atualmente no sistema. Por exemplo, um recurso package definido como installed no seu manifest acionará uma instalação do pacote no sistema se o pacote não tiver sido instalado anteriormente.

É assim que um recurso package se parece:

package { 'nginx':     ensure  => 'installed' } 

Você pode executar qualquer comando arbitrário declarando um recurso exec, como o seguinte:

exec { 'apt-get update':     command => '/usr/bin/apt-get update' } 

Observe que a parte apt-get update na primeira linha não é a declaração de comando real, mas um identificador para este recurso exclusivo. Frequentemente, precisamos fazer referência a outros recursos de dentro de um recurso e usamos o identificador deles para isso. Nesse caso, o identificador é apt-get update, mas poderia ser qualquer outra string.

Dependência de Recurso

Ao escrever manifests, é importante ter em mente que o Puppet não avalia os recursos na mesma ordem em que eles são definidos. Essa é uma fonte comum de confusão para quem está começando com o Puppet. Os recursos devem definir explicitamente a dependência entre si, caso contrário, não há garantia de qual recursos será avaliado e, consequentemente, executado primeiro.

Como um exemplo simples, digamos que você queira executar um comando, mas você precisa garantir que uma dependência seja instalada primeiro:

package { 'python-software-properties':     ensure => 'installed' }  exec { 'add-repository':     command => '/usr/bin/add-apt-repository ppa:ondrej/php5 -y'     require => Package['python-software-properties'] } 

A opção require recebe como parâmetro uma referência a outro recurso. Neste caso, estamos nos referindo ao recurso Package identificado como python-software-properties.

É importante notar que, enquanto usamos exec, package, e assim por diante para declarar recurso (com letras minúsculas), quando nos referimos a recurso definidos anteriormente, usamos Exec, Package e assim por diante (em maiúsculas).

Agora, digamos que você precise garantir que uma tarefa seja executada antes de outra. Para um caso como este, podemos usar a opção before:

package { 'curl':     ensure => 'installed'     before => Exec['install script'] }  exec { 'install script':     command => '/usr/bin/curl http://example.com/some-script.sh' 

Formato do Manifest

Os manifests são basicamente uma coleção de declarações de recursos, usando a extensão .pp. Abaixo você pode encontrar um exemplo de um playbook simples que executa duas tarefas: atualiza o cache do apt e instala o vim posteriormente:

exec { 'apt-get update':     command => '/usr/bin/apt-get update' }  package { 'vim':     ensure => 'installed'     require => Exec['apt-get update'] } 

Antes do final deste guia, veremos um exemplo mais real de um manifest, explicado em detalhes. A próxima seção fornecerá uma visão geral dos elementos e recursos mais importantes que podem ser usados para escrever manisfests do Puppet.

Escrevendo Manifests

Trabalhando com Variáveis

As variáveis podem ser definidas em qualquer ponto em um manifest. Os tipos mais comuns de variáveis são strings e matrizes ou arrays de strings, mas outros tipos também são suportados, como booleanos e hashes.

O exemplo abaixo define uma variável string que é usada posteriormente dentro de um recurso:

$package = "vim"  package { $package:    ensure => "installed" } 

Usando Loops

Os loops são normalmente usados para repetir uma tarefa usando diferentes valores de entrada. Por exemplo, em vez de criar 10 tarefas para instalar 10 pacotes diferentes, você pode criar uma única tarefa e usar um loop para repetir a tarefa com todos os pacotes diferentes que deseja instalar.

A maneira mais simples de repetir uma tarefa com valores diferentes no Puppet é usando arrays, como no exemplo abaixo:

$packages = ['vim', 'git', 'curl']  package { $packages:    ensure => "installed" } 

A partir da versão 4, o Puppet suporta maneiras adicionais de iterar as tarefas. O exemplo abaixo faz a mesma coisa que o exemplo anterior, mas desta vez usando o iterador each. Esta opção oferece mais flexibilidade para fazer um loop pelas definições de recursos:

$packages.each |String $package| {   package { $package:     ensure => "installed"   } } 

Usando Condicionais

Condicionais podem ser usadas para decidir dinamicamente se um bloco de código deve ou não ser executado, com base em uma variável ou em uma saída de um comando, por exemplo.

O Puppet suporta a maioria das estruturas condicionais que você pode encontrar nas linguagens de programação tradicionais, como as instruções if/else e case. Além disso, alguns recursos como o exec suportam atributos que funcionam como condicionais, mas aceitam apenas uma saída de comando como condição.

Digamos que você queira executar um comando com base em um fact. Nesse caso, como você deseja testar o valor de uma variável, é necessário usar uma das estruturas condicionais suportadas, como if/else:

if $osfamily != 'Debian' {  warning('This manifest is not supported on this OS.') } else {  notify { 'Good to go!': } } 

Outra situação comum é quando você deseja condicionar a execução de um comando com base na saída de outro comando. Para casos como esse, você pode usar onlyif ou unless, como no exemplo abaixo. Este comando será executado apenas quando a saída de /bin/which php for bem-sucedida, ou seja, o comando sair com o status 0:

exec { "Test":  command => "/bin/echo PHP is installed here > /tmp/test.txt",  onlyif => "/bin/which php" } 

Da mesma forma, unless irá executar o comando o tempo todo, exceto quando o comando sob unless sair com êxito:

exec { "Test":  command => "/bin/echo PHP is NOT installed here > /tmp/test.txt",  unless => "/bin/which php" } 

Trabalhando com Templates

Os templates geralmente são usados para definir arquivos de configuração, permitindo o uso de variáveis e outros recursos destinados a tornar esses arquivos mais versáteis e reutilizáveis. O Puppet suporta dois formatos diferentes para templates: Embedded Puppet (EPP) e Embedded Ruby (ERB). O formato EPP, no entanto, funciona apenas com versões recentes do Puppet (a partir da versão 4.0).

Abaixo está um exemplo de um template ERB para configurar um virtual host no Apache, usando uma variável para configurar o document root para este host:

<VirtualHost *:80>     ServerAdmin [email protected]     DocumentRoot <%= @doc_root %>      <Directory <%= @doc_root %>>         AllowOverride All         Require all granted     </Directory> </VirtualHost> 

Para aplicar o template, precisamos criar um recurso file que processe o conteúdo do template com o método template. É assim que você aplicaria esse template para substituir o virtual host padrão do Apache:

file { "/etc/apache2/sites-available/000-default.conf":     ensure => "present",     content => template("apache/vhost.erb")  }     

O Puppet faz algumas suposições ao lidar com arquivos locais, a fim de reforçar a organização e a modularidade. Nesse caso, o Puppet procuraria um arquivo de template vhost.erb dentro de uma pasta apache/templates, dentro do seu diretório de módulos.

Definindo e Acionando Serviços

Os recursos de serviço são usados para garantir que os serviços sejam inicializados e ativados. Eles também são usados para acionar a reinicialização do serviço.

Vamos levar em consideração o nosso exemplo anterior de uso de template, onde configuramos um virtual host no Apache. Se você deseja garantir que o Apache seja reiniciado após uma mudança no virtual host, primeiro crie um recurso service para o serviço Apache. É assim que esse recurso é definido no Puppet:

service { 'apache2':     ensure => running,     enable => true } 

Agora, ao definir o recurso, você precisa incluir uma opção notify para acionar uma reinicialização:

file { "/etc/apache2/sites-available/000-default.conf":     ensure => "present",     content => template("vhost.erb"),     notify => Service['apache2']  }  

Manifest de Exemplo

Agora, vamos dar uma olhada em um manifest que automatizará a instalação de um servidor Web Apache em um sistema Ubuntu 14.04, conforme discutido na introdução deste guia.

O exemplo completo, incluindo o arquivo de template para configurar o Apache e um arquivo HTML para ser servido pelo servidor web, pode ser encontrado no Github. A pasta também contém um arquivo Vagrant que permite testar o manifest em uma configuração simplificada, usando uma máquina virtual gerenciada pelo Vagrant.

Abaixo você pode encontrar o manifest completo:

default.pp

  • $doc_root = "/var/www/example"
  • exec { 'apt-get update':
  • command => '/usr/bin/apt-get update'
  • }
  • package { 'apache2':
  • ensure => "installed",
  • require => Exec['apt-get update']
  • }
  • file { $doc_root:
  • ensure => "directory",
  • owner => "www-data",
  • group => "www-data",
  • mode => 644
  • }
  • file { "$doc_root/index.html":
  • ensure => "present",
  • source => "puppet:///modules/main/index.html",
  • require => File[$doc_root]
  • }
  • file { "/etc/apache2/sites-available/000-default.conf":
  • ensure => "present",
  • content => template("main/vhost.erb"),
  • notify => Service['apache2'],
  • require => Package['apache2']
  • }
  • service { 'apache2':
  • ensure => running,
  • enable => true
  • }

Manifest Explicado

linha 1

O manifest começa com uma definição de variável, $doc_root. Essa variável é usada posteriormente em uma declaração de recurso.

linhas 3-5

Este recurso exec executa um comando apt-get update.

linhas 7-10

Este recurso package instala o pacote apache2, definindo que o recurso apt-get update é um requisito, o que significa que ele só será executado após a avaliação do recurso necessário.

linhas 12-17

Utilizamos um recurso file aqui para criar um novo diretório que servirá como document root. O recurso file pode ser usado para criar diretórios e arquivos, e também é usado para aplicar templates e copiar arquivos locais no servidor remoto. Esta tarefa pode ser executada em qualquer ponto do provisionamento, portanto, não precisamos definir nenhum require aqui.

linhas 19-23

Usamos outro recurso file aqui, desta vez para copiar nosso arquivo index.html local para o document root dentro do servidor. Usamos o parâmetro source para permitir que o Puppet saiba onde encontrar o arquivo original. Essa nomenclatura é baseada na maneira como o Puppet lida com arquivos locais; se você der uma olhada no repositório de exemplo do Github, você verá como a estrutura de diretórios deve ser criada para permitir que o Puppet encontre esse recurso. O diretório document root precisa ser criado antes da execução do recurso, e é por isso que incluímos uma opção require que faz referência ao recurso anterior.

linhas 25-30

Um novo recurso file é usado para aplicar o template do Apache e notificar o serviço para uma reinicialização. Neste exemplo, nosso provisionamento é organizado em um módulo chamado main, e é por isso que a origem do template é main/vhost.erb. Usamos uma instrução require para garantir que o recurso do template seja executado apenas após a instalação do pacote apache2, caso contrário, a estrutura de diretórios usada pelo Apache ainda não estará presente.

linhas 32-35

Finalmente, o recurso service declara o serviço apache2, que notificamos para reiniciar a partir do recurso que aplica o template de virtual host.

Conclusão

O Puppet é uma poderosa ferramenta de gerenciamento de configuração que usa uma DSL expressiva e personalizada para gerenciar recursos do servidor e automatizar tarefas. Sua linguagem oferece recursos avançados que podem dar flexibilidade extra às suas configurações de provisionamento; é importante lembrar que os recursos não são avaliados na mesma ordem em que são definidos e, por esse motivo, você precisa ter cuidado ao definir dependências entre os recursos para estabelecer a sequência de execução correta.

No próximo guia desta série, veremos o Chef, outra ferramenta poderosa de gerenciamento de configuração que aproveita a linguagem de programação Ruby para automatizar a administração e o provisionamento da infraestrutura.