Introdução ao Puppet

Puppet é um sistema de gestão de configuração. Ele é utilizado para trazer hosts ao estado desejado e manter esse estado.

Trabalho com Puppet há mais de cinco anos. Este texto é, de maneira essencial, uma compilação traduzida e reorganizada dos principais pontos da documentação oficial, que permitirá que novatos compreendam rapidamente os fundamentos do Puppet.

Introdução ao Puppet

Informações Básicas

O funcionamento do Puppet é baseado em um modelo cliente-servidor, embora exista a opção de operação sem servidor com funcionalidade limitada.

Utiliza um modelo de operação pull: por padrão, a cada meia hora, os clientes consultam o servidor em busca de configurações e as aplicam. Se você trabalhou com Ansible, lá é usado um modelo diferente, push: o administrador inicia o processo de aplicação da configuração, os clientes em si não aplicam nada.

Na interação em rede, é utilizado criptografia TLS bidirecional: o servidor e o cliente têm suas próprias chaves privadas e os certificados correspondentes. Normalmente, o servidor emite certificados para os clientes, mas em princípio, o uso de uma CA externa também é possível.

Introdução aos Manifestos

Na terminologia do Puppet conectam-se ao servidor Puppet os nós nós A configuração dos nós é escrita em manifestos numa linguagem de programação especial - Puppet DSL.

Puppet DSL é uma linguagem declarativa. Nela, o estado desejado do nó é descrito na forma da declaração de recursos individuais, por exemplo:

  • O arquivo existe e possui um conteúdo específico.
  • O pacote está instalado.
  • O serviço está em execução.

Os recursos podem estar interconectados:

  • Existem dependências, que influenciam a ordem de aplicação dos recursos.
    Por exemplo, "primeiro instale o pacote, em seguida ajuste o arquivo de configuração, depois inicie o serviço."
  • Existem notificações - se um recurso for alterado, ele envia notificações aos recursos que estão inscritos.
    Por exemplo, se o arquivo de configuração for alterado, é possível reiniciar automaticamente o serviço.

Além disso, na Puppet DSL há funções e variáveis, assim como operadores condicionais e seletores. Também são suportados vários mecanismos de template - EPP e ERB.

Puppet é escrito em Ruby, portanto, muitas construções e termos são derivados dele. Ruby permite estender o Puppet - acrescentando lógica complexa, novos tipos de recursos e funções.

Durante a execução do Puppet, os manifests para cada nó específico no servidor são compilados em um diretório. Diretório — é uma lista de recursos e suas inter-relações após calcular os valores de funções, variáveis e a expansão de operadores condicionais.

Sintaxe e estilo de codificação

Aqui estão as seções da documentação oficial que ajudarão a entender a sintaxe, se os exemplos fornecidos não forem suficientes:

Aqui está um exemplo de como um manifest se parece:

# Комментарии пишутся, как и много где, после решётки.
#
# Описание конфигурации ноды начинается с ключевого слова node,
# за которым следует селектор ноды — хостнейм (с доменом или без)
# или регулярное выражение для хостнеймов, или ключевое слово default.
#
# После этого в фигурных скобках описывается собственно конфигурация ноды.
#
# Одна и та же нода может попасть под несколько селекторов. Про приоритет
# селекторов написано в статье про синтаксис описания нод.
node 'hostname', 'f.q.d.n', /regexp/ {
  # Конфигурация по сути является перечислением ресурсов и их параметров.
  #
  # У каждого ресурса есть тип и название.
  #
  # Внимание: не может быть двух ресурсов одного типа с одинаковыми названиями!
  #
  # Описание ресурса начинается с его типа. Тип пишется в нижнем регистре.
  # Про разные типы ресурсов написано ниже.
  #
  # После типа в фигурных скобках пишется название ресурса, потом двоеточие,
  # дальше идёт опциональное перечисление параметров ресурса и их значений.
  # Значения параметров указываются через т.н. hash rocket (=>).
  resource { 'title':
    param1 => value1,
    param2 => value2,
    param3 => value3,
  }
}

Recuos e quebras de linha não são parte obrigatória do manifest, no entanto, existe um guia de estilo. Resumo:

  • Recuos de dois espaços; tabs não são utilizados.
  • Chaves são separadas por um espaço, os dois pontos não são separados por espaço.
  • Vígulas após cada parâmetro, incluindo o último. Cada parâmetro deve estar em uma linha separada. Há exceção para o caso sem parâmetros e um parâmetro: pode ser escrito em uma linha e sem vírgula (ou seja, resource { 'title': } e resource { 'title': param => value }).
  • Setas em parâmetros devem estar no mesmo nível.
  • Setas de inter-relação de recursos são escritas antes deles.

Localização de arquivos no servidor Puppet

Para explicações adicionais, introduzirei o conceito de "diretório raiz". O diretório raiz é aquele onde a configuração Puppet para um nó específico está localizada.

O diretório raiz varia dependendo da versão do Puppet e do uso de ambientes. Ambientes são conjuntos independentes de configuração, armazenados em diretórios separados. Normalmente, utilizados em combinação com o Git; neste caso, os ambientes são criados a partir de branches do Git. Assim, cada nó está em um ambiente ou outro. Isso é configurado no próprio nó ou no ENC, do qual falarei no próximo artigo.

  • Na terceira versão ("antigo Puppet"), o diretório base era /etc/puppet. O uso de ambientes é opcional — nós, por exemplo, não os utilizamos com o antigo Puppet. Se ambientes são utilizados, eles geralmente são armazenados em /etc/puppet/environments, o diretório raiz será o diretório do ambiente. Se ambientes não são utilizados, o diretório raiz será o base.
  • A partir da quarta versão ("novo Puppet"), o uso de ambientes se tornou obrigatório, e o diretório base foi movido para /etc/puppetlabs/code. Assim, os ambientes são armazenados em /etc/puppetlabs/code/environments, o diretório raiz é o diretório do ambiente.

Dentro do diretório raiz deve haver um subdiretório manifests, onde estão um ou mais manifestos descrevendo os nós. Além disso, deve haver um subdiretório modules, onde estão os módulos. O que são módulos, explicarei mais adiante. Além disso, no antigo Puppet, também pode haver um subdiretório files, onde estão vários arquivos que copiamos para os nós. No novo Puppet, todos os arquivos foram movidos para os módulos.

Os arquivos de manifesto têm a extensão .pp.

Um par de exemplos práticos

Descrição do nó e do recurso nele

No nó server1.testdomain deve ser criado um arquivo /etc/issue com o conteúdo Debian GNU/Linux n l. O arquivo deve pertencer ao usuário e ao grupo root, as permissões devem ser 644.

Escrevendo o manifesto:

node 'server1.testdomain' {   # bloco de configuração referente ao nó server1.testdomain
    file { 'ect/issue':   # descrevendo o arquivo /etc/issue
        ensure  => present,   # este arquivo deve existir
        content => 'Debian GNU/Linux n l',   # seu conteúdo deve ser este
        owner   => root,   # usuário proprietário
        group   => root,   # grupo proprietário
        mode    => '0644',   # permissões no arquivo. Elas são definidas como uma string (entre aspas), caso contrário, um número começando com 0 será interpretado como escrito no sistema octal, e tudo sairá como não planejado
    }
}

Interconexões de recursos no nó

No nó server2.testdomain deve ter o nginx em execução, funcionando com uma configuração preparada anteriormente.

Descompondo a tarefa:

  • É necessário que o pacote seja instalado nginx.
  • É necessário que os arquivos de configuração sejam copiados do servidor.
  • É necessário que o serviço seja iniciado nginx.
  • Em caso de atualização da configuração, o serviço deve ser reiniciado.

Escrevendo o manifesto:

node 'server2.testdomain' {   # bloco de configuração relacionado ao nó server2.testdomain
    package { 'nginx':   # descrevendo o pacote nginx
        ensure => installed,   # ele deve ser instalado
    }
  # A seta direta (->) indica que o recurso abaixo deve
  # ser criado após o recurso descrito acima.
  # Essas dependências são transitivas.
    -> file { '/etc/nginx':   # descrevendo o arquivo /etc/nginx
        ensure  => directory,   # isso deve ser um diretório
        source  => 'puppet:////modules/example/nginx-conf',   # seu conteúdo deve ser obtido do servidor Puppet no endereço indicado
        recurse => true,   # copiar arquivos de forma recursiva
        purge   => true,   # deve remover arquivos excessivos (aqueles que não estão na fonte)
        force   => true,   # remover diretórios excessivos
    }
  # A seta ondulada (~>) indica que o recurso abaixo deve
  # se inscrever para mudanças no recurso descrito acima.
  # A seta ondulada inclui a direção da seta (->).
    ~> service { 'nginx':   # descrevendo o serviço nginx
        ensure => running,   # ele deve estar em execução
        enable => true,   # deve iniciar automaticamente ao iniciar o sistema
    }
  # Quando um recurso do tipo serviço recebe uma notificação,
  # o serviço correspondente é reiniciado.
}

Para que isso funcione, é necessário um arranjo de arquivos semelhante no servidor Puppet:

/etc/puppetlabs/code/environments/production/ # (это для нового Паппета, для старого корневой директорией будет /etc/puppet)
├── manifests/
│   └── site.pp
└── modules/
    └── example/
        └── files/
            └── nginx-conf/
                ├── nginx.conf
                ├── mime.types
                └── conf.d/
                    └── some.conf

Tipos de recursos

A lista completa dos tipos de recursos suportados pode ser encontrada na documentação, aqui descreverei cinco tipos básicos que, na minha prática, são suficientes para resolver a maioria das tarefas.

arquivo

Gerencia arquivos, diretórios, links simbólicos, seu conteúdo e permissões de acesso.

Parâmetros:

  • nome do recurso — caminho para o arquivo (opcional)
  • path — caminho para o arquivo (se não estiver especificado no nome)
  • ensure — tipo de arquivo:
    • absent — remover arquivo
    • present — deve ser um arquivo de qualquer tipo (se não houver arquivo, um arquivo comum será criado)
    • arquivo — arquivo comum
    • directory — diretório
    • link — link simbólico
  • conteúdo — conteúdo do arquivo (apenas aplicável a arquivos comuns, não pode ser usado junto com source ou alvo)
  • source — link para o caminho de onde copiar o conteúdo do arquivo (não pode ser usado junto com conteúdo ou alvo). Pode ser especificado como um URI com o esquema puppet: (então os arquivos do servidor Puppet serão utilizados), ou com o esquema http: (espero que esteja claro o que acontecerá nesse caso), e até mesmo com o esquema file: ou como um caminho absoluto sem esquema (então será utilizado um arquivo do sistema local no nó)
  • alvo — onde o link simbólico deve apontar (não pode ser usado junto com conteúdo ou source)
  • owner — usuário a quem o arquivo deve pertencer
  • grupo — grupo ao qual o arquivo deve pertencer
  • modo — permissões do arquivo (em forma de string)
  • recurse — inclui o processamento recursivo de diretórios
  • purge — inclui a remoção de arquivos que não estão descritos no Puppet
  • force — inclui a remoção de diretórios que não estão descritos no Puppet

pacote

Instala e remove pacotes. Consegue processar notificações — reinstala o pacote se o parâmetro for especificado reinstall_on_refresh.

Parâmetros:

  • nome do recurso — nome do pacote (opcional)
  • nome — nome do pacote (se não especificado no nome)
  • provider — gerenciador de pacotes que deve ser usado
  • ensure — estado desejado do pacote:
    • present, installed — qualquer versão instalada
    • latest — última versão instalada
    • absent — removido (apt-get remove)
    • purged — removido junto com arquivos de configuração (apt-get purge)
    • held — versão do pacote bloqueada (apt-mark hold)
    • qualquer outra string — versão especificada instalada
  • reinstall_on_refresh — se verdadeiro, ao receber notificações, o pacote será reinstalado. Útil para distribuições baseadas em source, onde a recompilação de pacotes pode ser necessária ao mudar parâmetros de compilação. Por padrão falso.

serviço

Gerencia serviços. Consegue processar notificações — reinicia o serviço.

Parâmetros:

  • nome do recurso — serviço a ser gerenciado (opcional)
  • nome — serviço a ser gerenciado (se não especificado no nome)
  • ensure — estado desejado do serviço:
    • corrida — em execução
    • stopped — parado
  • enable — gerencia a capacidade de iniciar o serviço:
    • verdadeiro — auto inicialização ativada (systemctl enable)
    • mask — mascarado (systemctl mask)
    • falso — auto inicialização desativada (systemctl disable)
  • restart — comando para reiniciar o serviço
  • status — comando para verificar o status do serviço
  • hasrestart — indicar se o script de init do serviço suporta reinício. Se falso e o parâmetro especificado restart — usa o valor desse parâmetro. Se falso e o parâmetro restart não especificado — o serviço é parado e reiniciado (mas em systemd, usa-se o comando systemctl restart).
  • hasstatus — indicar se o script de init do serviço suporta o comando status. Se falso, então o valor do parâmetro é utilizado status. Por padrão verdadeiro.

exec

Executa comandos externos. Se não forem especificados parâmetros creates, onlyif, unless ou refreshonly, o comando será executado a cada execução do Puppet. Consegue processar notificações — executa o comando.

Parâmetros:

  • nome do recurso — comando a ser executado (opcional)
  • command — comando a ser executado (se não especificado no nome)
  • path — caminhos onde buscar o arquivo executável
  • onlyif — se o comando especificado neste parâmetro terminar com um código de retorno zero, o comando principal será executado
  • unless — se o comando especificado neste parâmetro terminar com um código de retorno diferente de zero, o comando principal será executado
  • creates — se o arquivo especificado neste parâmetro não existir, o comando principal será executado
  • refreshonly — se verdadeiro, o comando será executado apenas se este exec receber uma notificação de outros recursos
  • cwd — diretório a partir do qual executar o comando
  • user — usuário sob o qual executar o comando
  • provider — como executar o comando:
    • posix — apenas cria um processo filho, deve ser especificado path
    • shell — o comando é executado em um shell /bin/sh, pode não ser especificado path, pode-se usar globbing, pipes e outras funcionalidades do shell. Normalmente, é determinado automaticamente, se houver vários símbolos especiais (|, ;, &&, || e assim por diante).

cron

Gerencia jobs do cron.

Parâmetros:

  • nome do recurso — apenas algum identificador
  • ensure — estado do cron job:
    • present — criar se não existir
    • absent — remover se existir
  • command — qual comando executar
  • environment — em qual ambiente executar o comando (lista de variáveis de ambiente e seus valores separados por =)
  • user — de qual usuário executar o comando
  • minuto, hora, dia da semana, mês, dia do mês — quando executar o cron. Se algum desses atributos não for especificado, seu valor na crontab será *.

No Puppet 6.0 cron basicamente removido do pacote no puppetserver, portanto não há documentação no site geral. Mas ele está no pacote no puppet-agent, então não é necessário instalá-lo separadamente. A documentação sobre ele pode ser vista na documentação da quinta versão do Puppet, ou no GitHub.

Sobre os recursos em geral

Requisitos de unicidade dos recursos

O erro mais comum que encontramos é Declaração duplicada. Este erro ocorre quando dois ou mais recursos do mesmo tipo com o mesmo nome entram no catálogo.

Portanto, vou reiterar: não deve haver recursos do mesmo tipo com o mesmo nome (title) em manifests para um único nó!

Às vezes, pode ser necessário instalar pacotes com o mesmo nome, mas com gerenciadores de pacotes diferentes. Nesse caso, é necessário usar o parâmetro nome, para evitar erros:

package { 'ruby-mysql':
  ensure   => installed,
  name     => 'mysql',
  provider => 'gem',
}
package { 'python-mysql':
  ensure   => installed,
  name     => 'mysql',
  provider => 'pip',
}

Em outros tipos de recursos, existem parâmetros semelhantes que ajudam a evitar duplicações, nome u serviço, command u exec, e assim por diante.

Metaparametros

Alguns parâmetros especiais estão disponíveis para cada tipo de recurso, independentemente da sua entidade.

Lista completa de metaparametros na documentação do Puppet.

Lista resumida:

  • require — neste parâmetro, é indicado de quais recursos este recurso depende.
  • before — neste parâmetro, é indicado quais recursos dependem deste recurso.
  • subscribe — neste parâmetro, é indicado de quais recursos este recurso recebe notificações.
  • notify — neste parâmetro, é indicado quais recursos recebem notificações deste recurso.

Todos os metaparametros listados aceitam ou uma única referência a um recurso, ou um array de referências entre colchetes.

Referências a recursos

Uma referência a um recurso é apenas uma menção ao recurso. Elas são usadas principalmente para indicar dependências. Referenciar um recurso inexistente causará um erro de compilação.

A sintaxe de uma referência é a seguinte: tipo de recurso com inicial maiúscula (se o nome do tipo contém dois pontos duplos, cada parte entre os dois pontos deve ser escrita com inicial maiúscula), em seguida, entre colchetes, o nome do recurso (a capitalização do nome não é alterada!). Não deve haver espaços, os colchetes são escritos imediatamente após o nome do tipo.

Exemplo:

file { '/file1': ensure => present }
file { '/file2':
  ensure => directory,
  before => File['/file1'],
}
file { '/file3': ensure => absent }
File['/file1'] -> File['/file3']

Dependências e notificações

A documentação está aqui.

Como já foi mencionado anteriormente, as dependências simples entre recursos são transitivas. A propósito, tenha cuidado ao definir dependências — é possível criar dependências cíclicas, o que causará um erro de compilação.

Ao contrário das dependências, as notificações não são transitivas. Para as notificações, as seguintes regras se aplicam:

  • Se um recurso recebe uma notificação, ele é atualizado. As ações ao atualizar dependem do tipo de recurso — exec executa um comando, serviço reinicia um serviço, pacote reinstala um pacote. Se nenhuma ação de atualização é definida para o recurso, nada acontece.
  • Em uma única execução do Puppet, um recurso é atualizado no máximo uma vez. Isso é possível porque as notificações incluem dependências, e o grafo de dependências não contém ciclos.
  • Se o Puppet altera o estado de um recurso, o recurso envia notificações a todos os recursos que estão inscritos nele.
  • Se o recurso for atualizado, ele envia notificações a todos os recursos que estão inscritos nele.

Tratamento de parâmetros não especificados

Normalmente, se algum parâmetro de um recurso não possui um valor padrão e esse parâmetro não está especificado no manifesto, o Puppet não alterará essa propriedade no respectivo recurso no nó. Por exemplo, se um recurso do tipo arquivo não tiver um parâmetro especificado owner, o Puppet não alterará o proprietário do respectivo arquivo.

Introdução a classes, variáveis e definições

Suponha que temos vários nós que têm a mesma parte de configuração, mas também há diferenças — caso contrário, poderíamos descrever tudo isso em um único bloco. node {}. Claro, é possível simplesmente copiar as partes semelhantes da configuração, mas, em geral, essa é uma má solução — a configuração se torna extensa, e ao alterar a parte comum da configuração, teremos que corrigir a mesma coisa em muitos lugares. Além disso, é fácil cometer um erro, e, no geral, o princípio DRY (não se repita) não foi criado à toa.

Para resolver esse problema, existe uma construção chamada classe.

Classes

Classe — são um bloco nomeado de código Puppet. As classes são necessárias para reutilizar código.

Primeiro, a classe precisa ser descrita. A descrição por si só não adiciona nenhum recurso. A classe é descrita nos manifestos:

# Описание класса начинается с ключевого слова class и его названия.
# Дальше идёт тело класса в фигурных скобках.
class example_class {
    ...
}

Depois disso, a classe pode ser usada:

# первый вариант использования — в стиле ресурса с типом class
class { 'example_class': }
# второй вариант использования — с помощью функции include
include example_class
# про отличие этих двух вариантов будет рассказано дальше

Exemplo da tarefa anterior — vamos mover a instalação e configuração do nginx para uma classe:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => 'puppet:///modules/example/nginx-conf',
        recurse => true,
        purge  => true,
        force  => true,
    }
    ~> service { 'nginx':
        ensure => running,
        enable => true,
    }
}

node 'server2.testdomain' {
    include nginx_example
}

Variáveis

A classe do exemplo anterior não é flexível, porque sempre traz a mesma configuração do nginx. Vamos fazer para que o caminho da configuração se torne variável, assim essa classe pode ser usada para instalar o nginx com qualquer configuração.

Isso pode ser feito com a ajuda de variáveis.

Atenção: as variáveis no Puppet são imutáveis!

Além disso, você pode acessar uma variável somente após declará-la; caso contrário, o valor da variável será undef.

Exemplo de trabalho com variáveis:

# создание переменных
$variable = 'value'
$var2 = 1
$var3 = true
$var4 = undef
# использование переменных
$var5 = $var6
file { '/tmp/text': content => $variable }
# интерполяция переменных — раскрытие значения переменных в строках. Работает только в двойных кавычках!
$var6 = "Variable with name variable has value ${variable}"

No Puppet, existem espaços de nomes, e as variáveis, respectivamente, possuem escopo: uma variável com o mesmo nome pode ser definida em diferentes namespaces. Ao resolver o valor da variável, ela é buscada no namespace atual, depois no abrangente, e assim por diante.

Exemplos de namespace:

  • global — onde variáveis fora da descrição da classe ou nó vão;
  • namespace do nó na descrição do nó;
  • namespace da classe na descrição da classe.

Para evitar ambiguidade ao referenciar uma variável, pode-se especificar o namespace no nome da variável:

# переменная без пространства имён
$var
# переменная в глобальном пространстве имён
$::var
# переменная в пространстве имён класса
$classname::var
$::classname::var

Vamos concordar que o caminho para a configuração do nginx está na variável $nginx_conf_source. Então a classe ficará da seguinte forma:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => $nginx_conf_source,   # aqui usamos a variável em vez de uma string fixa
        recure => true,
        purge  => true,
        force  => true,
    }
    ~> service { 'nginx':
        ensure => running,
        enable => true,
    }
}

node 'server2.testdomain' {
    $nginx_conf_source = 'puppet:///modules/example/nginx-conf'
    include nginx_example
}

No entanto, o exemplo apresentado é ruim porque há um certo "conhecimento secreto" de que em algum lugar dentro da classe a variável com aquele nome é utilizada. É muito mais correto tornar esse conhecimento comum — as classes podem ter parâmetros.

Parâmetros de classe — são variáveis no namespace da classe, que são definidas no cabeçalho da classe e podem ser usadas como variáveis comuns no corpo da classe. Os valores dos parâmetros são especificados ao usar a classe no manifesto.

Um parâmetro pode ter um valor padrão. Se um parâmetro não tiver um valor padrão e nenhum valor for especificado ao usá-lo, isso causará um erro de compilação.

Vamos parametrizar a classe do exemplo acima e adicionar dois parâmetros: o primeiro, obrigatória — o caminho para a configuração, e o segundo, opcional — o nome do pacote com nginx (no Debian, por exemplo, há pacotes nginx, nginx-light, nginx-full).

# переменные описываются сразу после имени класса в круглых скобках
class nginx_example (
  $conf_source,
  $package_name = 'nginx-light', # параметр со значением по умолчанию
) {
  package { $package_name:
    ensure => installed,
  }
  -> file { '/etc/nginx':
    ensure  => directory,
    source  => $conf_source,
    recurse => true,
    purge   => true,
    force   => true,
  }
  ~> service { 'nginx':
    ensure => running,
    enable => true,
  }
}

node 'server2.testdomain' {
  # если мы хотим задать параметры класса, функция include не подойдёт* — нужно использовать resource-style declaration
  # *на самом деле подойдёт, но про это расскажу в следующей серии. Ключевое слово "Hiera".
  class { 'nginx_example':
    conf_source => 'puppet:///modules/example/nginx-conf',   # задаём параметры класса точно так же, как параметры для других ресурсов
  }
}

No Puppet, as variáveis são tipadas. Existem muitos tipos de dados. Os tipos de dados são geralmente usados para validar os valores dos parâmetros passados às classes e definições. Se o parâmetro passado não corresponder ao tipo especificado, ocorrerá um erro de compilação.

O tipo é escrito diretamente antes do nome do parâmetro:

classe exemplo (
  String $param1,
  Integer $param2,
  Array $param3,
  Hash $param4,
  Hash[String, String] $param5,
) {
  ...
}

Classes: include classname vs class{‘classname’:}

Cada classe é um recurso do tipo classe. Assim como em qualquer outro tipo de recurso, não pode haver duas instâncias da mesma classe em um único nó.

Se tentar adicionar uma classe ao mesmo nó duas vezes usando class { 'classname':} (não importa se com parâmetros diferentes ou iguais), haverá um erro de compilação. No entanto, ao usar uma classe no estilo de recurso, você pode definir claramente todos os seus parâmetros no manifesto.

No entanto, se usar include, você pode adicionar a classe quantas vezes quiser. O que acontece é que include — é uma função idempotente que verifica se a classe já está no diretório. Se a classe não estiver no diretório, ela é adicionada, caso contrário, nada é feito. Mas no caso de usar include não é possível definir parâmetros da classe durante a declaração da classe — todos os parâmetros obrigatórios devem ser fornecidos a partir de uma fonte de dados externa — Hiera ou ENC. Falaremos sobre eles no próximo artigo.

Defines

Como mencionado no bloco anterior, a mesma classe não pode estar em um nó mais de uma vez. No entanto, em alguns casos, é necessário poder aplicar o mesmo bloco de código com parâmetros diferentes em um único nó. Em outras palavras, há uma necessidade de um próprio tipo de recurso.

Por exemplo, para instalar o módulo PHP, fazemos o seguinte na Avito:

  1. Instalamos o pacote com este módulo.
  2. Criamos um arquivo de configuração para este módulo.
  3. Criamos um symlink para a configuração do php-fpm.
  4. Criamos um symlink para a configuração do php cli.

Nesses casos, é usada uma estrutura como define (define, defined type, defined resource type). Define é semelhante a uma classe, mas há diferenças: em primeiro lugar, cada define é um tipo de recurso, e não um recurso; em segundo lugar, cada define tem um parâmetro implícito $title, onde o nome do recurso é colocado ao ser declarado. Assim como no caso das classes, um define deve ser descrito primeiro antes de ser utilizado.

Exemplo simplificado com um módulo para PHP:

defina php74::módulo (
  $php_module_name = $title,
  $php_package_name = "php7.4-${title}",
  $version = 'instalado',
  $priority = '20',
  $data = "extension=${title}.son",
  $php_module_path = 'etc/php/7.4/mods-available',
) {
  pacote { $php_package_name:
    garantir          => $version,
    install_options => ['-o', 'DPkg::NoTriggers=true'],  # os gatilhos dos pacotes php do debian geram links simbólicos e reiniciam o serviço php-fpm - não precisamos disso, pois gerenciamos tanto os links simbólicos quanto o serviço com Puppet
  }
  -> arquivo { "${php_module_path}/${php_module_name}.ini":
    garantir  => $ensure,
    conteúdo => $data,
  }
  arquivo { "etc/php/7.4/cli/conf.d/${priority}-${php_module_name}.ini":
    garantir  => link,
    destino  => "${php_module_path}/${php_module_name}.ini",
  }
  arquivo { "etc/php/7.4/fpm/conf.d/${priority}-${php_module_name}.ini":
    garantir  => link,
    destino  => "${php_module_path}/${php_module_name}.ini",
  }
}

nó server3.testdomain {
  php74::módulo { 'sqlite3': }
  php74::módulo { 'amqp': php_package_name => 'php-amqp' }
  php74::módulo { 'msgpack': priority => '10' }
}

No define, é mais fácil pegar o erro Declaração duplicada. Isso acontece se houver um recurso com nome constante no define, e em algum nó existem duas ou mais instâncias desse define.

Para se proteger disso, é simples: todos os recursos dentro do define devem ter nomes que dependem de $title. Como alternativa, a adição idempotente de recursos; no caso mais simples, basta mover os recursos comuns para todas as instâncias do define para uma classe separada e incluir essa classe no define - a função include é idempotente.

Existem outras maneiras de alcançar a idempotência ao adicionar recursos, especialmente usando funções definido e garantir_recursos, mas falarei sobre isso na próxima série.

Dependências e notificações para classes e defines

Classes e defines adicionam as seguintes regras para o gerenciamento de dependências e notificações:

  • a dependência de uma classe/define adiciona dependências de todos os recursos da classe/define;
  • a dependência de uma classe/define adiciona dependências a todos os recursos da classe/define;
  • a notificação de uma classe/define notifica todos os recursos da classe/define;
  • a assinatura em uma classe/define assina todos os recursos da classe/define.

Operadores condicionais e seletores

A documentação está aqui.

if

Aqui é simples:

se CONDIÇÃO1 {
  ...
} elsif CONDIÇÃO2 {
  ...
} senão {
  ...
}

unless

unless — é como if ao contrário: o bloco de código será executado se a condição for falsa.

unless CONDIÇÃO {
  ...
}

caso

Aqui também não há nada complicado. Como valores, você pode usar valores comuns (strings, números e assim por diante), expressões regulares, bem como tipos de dados.

case EXPRESSÃO {
  VALOR1: { ... }
  VALOR2, VALOR3: { ... }
  padrão: { ... }
}

Seletores

Um seletor é uma construção da linguagem, semelhante a caso, mas em vez de executar um bloco de código, ele retorna um valor.

$var = $othervar ? { 'val1' => 1, 'val2' => 2, padrão => 3 }

Módulos

Quando a configuração é pequena, é fácil mantê-la em um único manifesto. Mas quanto mais configuração descrevemos, mais classes e nós aparecem no manifesto, tornando-se complicado de manusear.

Além disso, há o problema de reutilização de código - quando todo o código está em um único manifesto, é difícil compartilhar esse código com outros. Para resolver esses dois problemas, o Puppet possui uma entidade chamada módulos.

Módulos — são conjuntos de classes, defines e outras entidades do Puppet, movidos para um diretório separado. Em outras palavras, um módulo é um pedaço independente da lógica do Puppet. Por exemplo, pode haver um módulo para trabalhar com nginx, e ele conterá apenas o que é necessário para trabalhar com nginx, e pode haver um módulo para trabalhar com PHP, e assim por diante.

Os módulos são versionados, e também suportam dependências entre módulos. Existe um repositório aberto de módulos — Puppet Forge.

No servidor Puppet, os módulos estão localizados no subdiretório modules do diretório raiz. Dentro de cada módulo, o esquema padrão de diretórios é — manifests, files, templates, lib, e assim por diante.

Estrutura de arquivos do módulo

Na raiz do módulo, podem haver os seguintes diretórios com nomes descritivos:

  • manifests — contendo os manifestos
  • files — contendo os arquivos
  • templates — contendo os templates
  • lib — contendo código Ruby

Esta não é uma lista completa de diretórios e arquivos, mas é suficiente para este artigo por enquanto.

Nomes de recursos e nomes de arquivos no módulo

A documentação está aqui.

Recursos (classes, defines) em um módulo não podem ser nomeados arbitrariamente. Além disso, há uma correspondência direta entre o nome do recurso e o nome do arquivo em que o Puppet procurará a descrição desse recurso. Se as regras de nomenclatura forem violadas, o Puppet simplesmente não encontrará a descrição dos recursos, resultando em um erro de compilação.

As regras são simples:

  • Todos os recursos em um módulo devem estar no namespace do módulo. Se o módulo se chama foo, todos os recursos nele devem ser chamados foo::, ou simplesmente foo.
  • O recurso com o nome do módulo deve estar no arquivo init.pp.
  • Para os outros recursos, o esquema de nomeação de arquivos é o seguinte:
    • o prefixo com o nome do módulo é descartado
    • todos os dois pontos, se houver, são substituídos por barras
    • a extensão é adicionada .pp

Vou demonstrar com um exemplo. Suponha que estou escrevendo um módulo nginx. Ele possui os seguintes recursos:

  • classe nginx descrito no manifesto init.pp;
  • classe nginx::service descrito no manifesto service.pp;
  • define nginx::server descrito no manifesto server.pp;
  • define nginx::server::location descrito no manifesto server/location.pp.

Modelos

Certamente você já sabe o que são templates, não vou me alongar aqui. Mas, por precaução, deixarei um link para a Wikipédia.

Como usar templates: o valor do template pode ser revelado pela função template, que recebe o caminho para o template. Para recursos do tipo arquivo usamos junto com o parâmetro conteúdo. Por exemplo, assim:

file { '/tmp/example': content => template('modulename/templatename.erb')

Um caminho no formato / implica que o arquivo está em /modules//templates/.

Além disso, existe a função inline_template — que recebe o texto do template, e não o nome do arquivo.

Dentro dos templates, é possível usar todas as variáveis do Puppet no escopo atual.

O Puppet suporta templates nos formatos ERB e EPP:

Um resumo sobre ERB

Estruturas de controle:

  • <%= ВЫРАЖЕНИЕ %> — insere o valor da expressão
  • <% ВЫРАЖЕНИЕ %> — avalia o valor da expressão (sem inseri-lo). Os operadores condicionais (if) e loops (each) entram aqui.
  • <%# КОММЕНТАРИЙ %>

Expressões em ERB são escritas em Ruby (na verdade, ERB significa Ruby Embutido).

Para acessar variáveis do manifesto, é necessário adicionar @ ao nome da variável. Para remover a quebra de linha que aparece após a estrutura de controle, deve-se usar a tag de fechamento -%>.

Exemplo de uso do template

Suponha que estou escrevendo um módulo para gerenciar o ZooKeeper. A classe responsável por criar a configuração se parece mais ou menos assim:

class zookeeper::configure (
  Array[String] $nodes,
  Integer $port_client,
  Integer $port_quorum,
  Integer $port_leader,
  Hash[String, Any] $properties,
  String $datadir,
) {
  file { '/etc/zookeeper/conf/zoo.cfg':
    ensure  => present,
    content => template('zookeeper/zoo.cfg.erb'),
  }
}

E o correspondente template zoo.cfg.erb — é assim:

0 -%>

server.=::;



dataDir=


=

Fatos e variáveis internas

Frequentemente, uma parte específica da configuração depende do que está acontecendo no nó naquele momento. Por exemplo, dependendo da versão do Debian instalada, é necessário instalar uma versão específica do pacote. É possível fazer esse monitoramento manualmente, reescrevendo os manifests em caso de alteração dos nós. No entanto, essa abordagem não é séria; a automação é muito melhor.

Para obter informações sobre nós no Puppet, existe um mecanismo chamado fatos. Fatos são informações sobre o nó, disponíveis nos manifests como variáveis comuns no espaço de nomes global. Por exemplo, o nome do host, a versão do sistema operacional, a arquitetura do processador, a lista de usuários, a lista de interfaces de rede e seus endereços, e muito mais. Fatos estão disponíveis nos manifests e templates como variáveis comuns.

Exemplo de uso de fatos:

notify { "Executando o SO ${facts['os']['name']} versão ${facts['os']['release']['full']}": }
# O recurso do tipo notify simplesmente exibe uma mensagem no log

Falando formalmente, um fato tem um nome (string) e um valor (diversos tipos disponíveis: strings, arrays, dicionários). Existem um conjunto de fatos internos. Também é possível escrever os seus próprios. Os coletores de fatos são descritos como funções em Ruby, ou como arquivos executáveis. Além disso, os fatos podem ser apresentados na forma de arquivos de texto com dados nos nós.

Durante a operação, o agente Puppet primeiro copia do Puppet server para o nó todos os coletores de fatos disponíveis, depois os executa e envia os fatos coletados de volta para o servidor; só após isso o servidor começa a compilar o catálogo.

Fatos na forma de arquivos executáveis

Esses fatos são colocados nos módulos no diretório facts.d. Claro, os arquivos devem ser executáveis. Ao serem executados, devem mostrar na saída padrão informações em formato YAML ou na forma 'chave=valor'.

Não se esqueça que os fatos se aplicam a todos os nós que estão sob o controle do Puppet server onde seu módulo é implantado. Portanto, no script, certifique-se de que o sistema tenha todos os programas e arquivos necessários para o funcionamento do seu fato.

#!/bin/sh
echo "testfact=success"
#!/bin/sh
echo '{"testyamlfact":"success"}'

Fatos em Ruby

Esses fatos são colocados nos módulos no diretório lib/facter.

# всё начинается с вызова функции Facter.add с именем факта и блоком кода
Facter.add('ladvd') do
# в блоках confine описываются условия применимости факта — код внутри блока должен вернуть true, иначе значение факта не вычисляется и не возвращается
  confine do
    Facter::Core::Execution.which('ladvdc') # проверим, что в PATH есть такой исполняемый файл
  end
  confine do
    File.socket?('/var/run/ladvd.sock') # проверим, что есть такой UNIX-domain socket
  end
# в блоке setcode происходит собственно вычисление значения факта
  setcode do
    hash = {}
    if (out = Facter::Core::Execution.execute('ladvdc -b'))
      out.split.each do |l|
        line = l.split('=')
        next if line.length != 2
        name, value = line
        hash[name.strip.downcase.tr(' ', '_')] = value.strip.chomp(''').reverse.chomp(''').reverse
      end
    end
    hash  # значение последнего выражения в блоке setcode является значением факта
  end
end

Fatos textuais

Esses fatos são colocados nos nós no diretório /etc/facter/facts.d no antigo Puppet ou /etc/puppetlabs/facts.d no novo Puppet.

examplefact=examplevalue
---
examplefact2: examplevalue2
anotherfact: anothervalue

Acesso aos fatos

Você pode acessar os fatos de duas maneiras:

  • através do dicionário $facts: $facts['fqdn'];
  • usando o nome do fato como um nome de variável: $fqdn.

É melhor usar o dicionário $facts, e ainda melhor especificar o namespace global ($::facts).

Aqui está a seção necessária da documentação.

Variáveis integradas

Além dos fatos, existem também algumas variáveis, disponíveis no espaço de nomes global.

  • fatos confiáveis — variáveis que são obtidas a partir do certificado do cliente (como o certificado é geralmente emitido no servidor Puppet, o agente não pode simplesmente mudar seu certificado, portanto as variáveis são 'confiáveis'): nome do certificado, nome do host e domínio, extensões do certificado.
  • fatos do servidor — variáveis que se referem a informações sobre o servidor — versão, nome, endereço IP do servidor, ambiente.
  • fatos do agente — variáveis adicionadas diretamente pelo puppet-agent, e não pelo facter — nome do certificado, versão do agente, versão do Puppet.
  • variáveis do mestre — variáveis do puppetmaster (sic!). Lá, aproximadamente o mesmo que em fatos do servidor, além de valores de parâmetros de configuração disponíveis.
  • variáveis do compilador — variáveis do compilador que variam em cada escopo: nome do módulo atual e nome do módulo de onde foi feita a referência ao objeto atual. Elas podem ser usadas, por exemplo, para verificar se suas classes privadas não estão sendo usadas diretamente de outros módulos.

Adendo 1: como executar e depurar tudo isso?

O artigo teve muitos exemplos de código Puppet, mas não explicou como executar esse código. Bem, vou corrigir isso.

Para fazer o Puppet funcionar, basta ter um agente, mas na maioria dos casos, um servidor também será necessário.

Agente

Pelo menos a partir da versão cinco, os pacotes puppet-agent do repositório oficial da Puppetlabs contêm todas as dependências (ruby e os respectivos gemas), portanto não há dificuldades na instalação (estou falando de distribuições baseadas em Debian — distribuições baseadas em RPM não usamos).

No caso mais simples, para aplicar a configuração do puppet, basta executar o agente em modo sem servidor: desde que o código Puppet tenha sido copiado para o nó, você executa puppet apply:

atikhonov@atikhonov ~/puppet-test $ cat helloworld.pp 
node default {
    notify { 'Hello world!': }
}
atikhonov@atikhonov ~/puppet-test $ puppet apply helloworld.pp 
Aviso: Catálogo compilado para atikhonov.localdomain no ambiente de produção em 0,01 segundos
Aviso: Olá, mundo!
Aviso: /Stage[main]/Main/Node[default]/Notify[Hello world!]/message: definiu 'message' como 'Hello world!'
Aviso: Catálogo aplicado em 0,01 segundos

Claro, é melhor subir o servidor e executar os agentes nos nós em modo daemon — assim, a cada meia hora eles aplicarão a configuração baixada do servidor.

Você pode simular um modelo de push — acesse o nó que lhe interessa e execute sudo puppet agent -t. A opção -t (--test) na verdade inclui várias opções que podem ser ativadas separadamente. Entre essas opções estão as seguintes:

  • não funcionar em modo daemon (por padrão, o agente é executado em modo daemon);
  • terminar após aplicar o catálogo (por padrão, o agente continuará em execução e aplicará a configuração a cada meia hora);
  • escrever um log detalhado de operações;
  • mostrar alterações nos arquivos.

O agente possui um modo de operação sem alterações — pode ser utilizado quando você não tem certeza de que escreveu uma configuração correta e deseja verificar o que exatamente o agente mudará durante a execução. Esse modo é ativado com o parâmetro --noop na linha de comando: sudo puppet agent -t --noop.

Além disso, você pode ativar um log de depuração — nele, o puppet registra todas as ações que executa: sobre o recurso que está processando no momento, sobre os parâmetros desse recurso, sobre quais programas ele executa. Naturalmente, este é o parâmetro --debug.

Servidor

Não vou discutir a configuração completa do puppetserver e a implantação do código nele neste artigo, apenas direi que, por padrão, uma versão totalmente funcional do servidor é instalada, não requerendo configuração adicional para operar com um pequeno número de nós (digamos, até cem). Um número maior de nós já exigirá ajustes — por padrão, o puppetserver inicia no máximo quatro trabalhadores, para maior desempenho, é necessário aumentar esse número e não esquecer de aumentar os limites de memória, caso contrário, o servidor passará a maior parte do tempo coletando lixo.

A implantação do código — se precisa de algo rápido e simples, veja (em r10k)[https://github.com/puppetlabs/r10k], para pequenas instalações isso deve ser suficiente.

Complemento 2: recomendações para escrita de código

  1. Leve toda a lógica para classes e definições.
  2. Mantenha classes e defines em módulos, e não em manifests com descrição de nós.
  3. Use fatos.
  4. Evite usar ifs baseados em hostnames.
  5. Não hesite em adicionar parâmetros para classes e defines — é melhor do que lógica implícita escondida no corpo da classe/define.

E por que recomendo isso — explicarei no próximo artigo.

Conclusão

Com isso, finalizamos a introdução. No próximo artigo, falarei sobre Hiera, ENC e PuppetDB.

Apenas usuários registrados podem participar da pesquisa. Entre, por favor.

Na verdade, há muito mais material — posso escrever artigos sobre os seguintes temas, votem sobre o que vocês gostariam de ler:

  • 59,1%Advanced puppet constructs — alguns conceitos mais avançados: loops, mapeamento e outras expressões lambda, coletores de recursos, recursos exportáveis e interação entre hosts via Puppet, tags, provedores, tipos de dados abstratos.
  • 31,8%“Eu sou o admin da mamãe” ou como nós na Avito unimos vários servidores Puppet de versões diferentes, e em geral parte sobre administração de servidores Puppet.
  • 81,8%Como escrevemos código Puppet: estrutura de ferramentas, documentação, testes, CI/CD.

22 usuários votaram. 9 usuários se abstiveram.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster