Introdução ao fantoche

Puppet é um sistema de gerenciamento de configuração. É usado para levar os hosts ao estado desejado e mantê-lo.

Trabalho com o Puppet há mais de cinco anos. Este texto é essencialmente uma compilação traduzida e reordenada de pontos-chave da documentação oficial, que permitirá aos iniciantes compreender rapidamente a essência do Puppet.

Introdução ao fantoche

Informação básica

O sistema operacional do Puppet é cliente-servidor, embora também suporte operação sem servidor com funcionalidade limitada.

É usado um modelo de operação pull: por padrão, uma vez a cada meia hora, os clientes entram em contato com o servidor para obter uma configuração e aplicá-la. Se você trabalhou com Ansible, eles usam um modelo push diferente: o administrador inicia o processo de aplicação da configuração, os próprios clientes não aplicarão nada.

Durante a comunicação de rede, é usada criptografia TLS bidirecional: o servidor e o cliente possuem suas próprias chaves privadas e certificados correspondentes. Normalmente o servidor emite certificados para clientes, mas em princípio é possível usar uma CA externa.

Introdução aos manifestos

Na terminologia do fantoche para o servidor fantoche conectar nós (nós). A configuração dos nós está escrita em manifestos em uma linguagem de programação especial - Puppet DSL.

Puppet DSL é uma linguagem declarativa. Descreve o estado desejado do nó na forma de declarações de recursos individuais, por exemplo:

  • O arquivo existe e possui conteúdo específico.
  • O pacote está instalado.
  • O serviço foi iniciado.

Os recursos podem ser interligados:

  • Existem dependências, elas afetam a ordem em que os recursos são usados.
    Por exemplo, “primeiro instale o pacote, depois edite o arquivo de configuração e, em seguida, inicie o serviço”.
  • Existem notificações - se um recurso foi alterado, ele envia notificações para os recursos inscritos nele.
    Por exemplo, se o arquivo de configuração for alterado, você poderá reiniciar automaticamente o serviço.

Além disso, a DSL Puppet possui funções e variáveis, bem como instruções condicionais e seletores. Vários mecanismos de modelos também são suportados – EPP e ERB.

O Puppet é escrito em Ruby, então muitas das construções e termos são retirados daí. Ruby permite expandir o Puppet - adicionar lógica complexa, novos tipos de recursos, funções.

Enquanto o Puppet está em execução, os manifestos de cada nó específico no servidor são compilados em um diretório. Каталог é uma lista de recursos e seus relacionamentos após calcular o valor de funções, variáveis ​​e expansão de declarações condicionais.

Sintaxe e estilo de código

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

Aqui está um exemplo da aparência do manifesto:

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

Recuo e quebras de linha não são partes obrigatórias do manifesto, mas há uma recomendação guia de estilo. Resumo:

  • Recuos de dois espaços, tabulações não são usadas.
  • As chaves são separadas por um espaço; os dois pontos não são separados por um espaço.
  • Vírgulas após cada parâmetro, incluindo o último. Cada parâmetro está em uma linha separada. Uma exceção é feita para o caso sem parâmetros e um parâmetro: você pode escrever em uma linha e sem vírgula (ou seja, resource { 'title': } и resource { 'title': param => value }).
  • As setas nos parâmetros devem estar no mesmo nível.
  • As setas de relacionamento de recursos estão escritas na frente deles.

Localização dos arquivos no pappetserver

Para maiores explicações, apresentarei o conceito de “diretório raiz”. O diretório raiz é o diretório que contém a configuração do Puppet para um nó específico.

O diretório raiz varia dependendo da versão do Puppet e dos ambientes usados. Os ambientes são conjuntos independentes de configuração armazenados em diretórios separados. Geralmente usado em combinação com git, caso em que os ambientes são criados a partir de ramificações git. Conseqüentemente, cada nó está localizado em um ambiente ou outro. Isso pode ser configurado no próprio nó, ou no ENC, do qual falarei no próximo artigo.

  • Na terceira versão ("old Puppet") o diretório base era /etc/puppet. O uso de ambientes é opcional – por exemplo, não os utilizamos com o antigo Puppet. Se forem usados ​​ambientes, eles geralmente são armazenados em /etc/puppet/environments, o diretório raiz será o diretório do ambiente. Se os ambientes não forem usados, o diretório raiz será o diretório base.
  • A partir da quarta versão (“novo Puppet”), o uso de ambientes tornou-se 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.

Deve haver um subdiretório no diretório raiz manifests, que contém um ou mais manifestos que descrevem os nós. Além disso, deve haver um subdiretório modules, que contém os módulos. Direi quais são os módulos um pouco mais tarde. Além disso, o antigo Puppet também pode ter um subdiretório files, que contém vários arquivos que copiamos para os nós. No novo Puppet, todos os arquivos são colocados em módulos.

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

Alguns exemplos de combate

Descrição do nó e recurso nele

No nó server1.testdomain um arquivo deve ser criado /etc/issue com conteúdo Debian GNU/Linux n l. O arquivo deve pertencer a um usuário e grupo root, os direitos de acesso devem ser 644.

Escrevemos um manifesto:

node 'server1.testdomain' {   # блок конфигурации, относящийся к ноде server1.testdomain
    file { '/etc/issue':   # описываем файл /etc/issue
        ensure  => present,   # этот файл должен существовать
        content => 'Debian GNU/Linux n l',   # у него должно быть такое содержимое
        owner   => root,   # пользователь-владелец
        group   => root,   # группа-владелец
        mode    => '0644',   # права на файл. Они заданы в виде строки (в кавычках), потому что иначе число с 0 в начале будет воспринято как записанное в восьмеричной системе, и всё пойдёт не так, как задумано
    }
}

Relacionamentos entre recursos em um nó

No nó server2.testdomain O nginx deve estar rodando, funcionando com uma configuração previamente preparada.

Vamos decompor o problema:

  • O pacote precisa ser instalado nginx.
  • É necessário que os arquivos de configuração sejam copiados do servidor.
  • O serviço precisa estar em execução nginx.
  • Se a configuração for atualizada, o serviço deverá ser reiniciado.

Escrevemos um manifesto:

node 'server2.testdomain' {   # блок конфигурации, относящийся к ноде server2.testdomain
    package { 'nginx':   # описываем пакет nginx
        ensure => installed,   # он должен быть установлен
    }
  # Прямая стрелка (->) говорит о том, что ресурс ниже должен
  # создаваться после ресурса, описанного выше.
  # Такие зависимости транзитивны.
    -> file { '/etc/nginx':   # описываем файл /etc/nginx
        ensure  => directory,   # это должна быть директория
        source  => 'puppet:///modules/example/nginx-conf',   # её содержимое нужно брать с паппет-сервера по указанному адресу
        recurse => true,   # копировать файлы рекурсивно
        purge   => true,   # нужно удалять лишние файлы (те, которых нет в источнике)
        force   => true,   # удалять лишние директории
    }
  # Волнистая стрелка (~>) говорит о том, что ресурс ниже должен
  # подписаться на изменения ресурса, описанного выше.
  # Волнистая стрелка включает в себя прямую (->).
    ~> service { 'nginx':   # описываем сервис nginx
        ensure => running,   # он должен быть запущен
        enable => true,   # его нужно запускать автоматически при старте системы
    }
  # Когда ресурс типа service получает уведомление,
  # соответствующий сервис перезапускается.
}

Para que isso funcione, você precisa aproximadamente do seguinte local de arquivo no servidor fantoche:

/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

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

lima

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

opções:

  • nome do recurso — caminho para o arquivo (opcional)
  • caminho — caminho para o arquivo (se não estiver especificado no nome)
  • garantir - tipo de arquivo:
    • absent - excluir um arquivo
    • present — deve haver um arquivo de qualquer tipo (se não houver arquivo, será criado um arquivo normal)
    • file - arquivo normal
    • directory - diretório
    • link - link simbólico
  • conteúdo — conteúdo do arquivo (adequado apenas para arquivos normais, não pode ser usado junto com fonte ou alvo)
  • fonte — um link para o caminho do qual você deseja copiar o conteúdo do arquivo (não pode ser usado junto com conteúdo ou alvo). Pode ser especificado como um URI com um esquema puppet: (então os arquivos do servidor fantoche serão usados), e com o esquema http: (espero que esteja claro o que acontecerá neste caso), e mesmo com o diagrama file: ou como um caminho absoluto sem esquema (então será usado o arquivo do FS local no nó)
  • alvo — para onde o link simbólico deve apontar (não pode ser usado junto com conteúdo ou fonte)
  • proprietário — o usuário que deve possuir o arquivo
  • grupo — o grupo ao qual o arquivo deve pertencer
  • modo — permissões de arquivo (como uma string)
  • recurso - permite o processamento recursivo de diretórios
  • purga - permite a exclusão de arquivos que não estão descritos no Puppet
  • força - permite a exclusão de diretórios que não estão descritos no Puppet

pacote

Instala e remove pacotes. Capaz de lidar com notificações – reinstala o pacote se o parâmetro for especificado reinstalar_on_refresh.

opções:

  • nome do recurso — nome do pacote (opcional)
  • nome — nome do pacote (se não especificado no nome)
  • provedor — gerenciador de pacotes para usar
  • garantir — estado desejado do pacote:
    • present, installed - qualquer versão instalada
    • latest - última versão instalada
    • absent - excluído (apt-get remove)
    • purged — excluído junto com os arquivos de configuração (apt-get purge)
    • held - a versão do pacote está bloqueada (apt-mark hold)
    • любая другая строка — a versão especificada está instalada
  • reinstalar_on_refresh - se um true, após o recebimento da notificação, o pacote será reinstalado. Útil para distribuições baseadas em código-fonte, onde a reconstrução de pacotes pode ser necessária ao alterar os parâmetros de construção. Padrão false.

serviço

Gerencia serviços. Capaz de processar notificações – reinicia o serviço.

opções:

  • nome do recurso — serviço a ser gerenciado (opcional)
  • nome — o serviço que precisa ser gerenciado (se não estiver especificado no nome)
  • garantir — estado desejado do serviço:
    • running - lançado
    • stopped - parou
  • permitir — controla a capacidade de iniciar o serviço:
    • true — a execução automática está habilitada (systemctl enable)
    • mask - disfarçado (systemctl mask)
    • false — a execução automática está desativada (systemctl disable)
  • restart - comando para reiniciar o serviço
  • estado — comando para verificar o status do serviço
  • hasrestart — indique se o script de inicialização do serviço oferece suporte à reinicialização. Se false e o parâmetro é especificado restart — o valor deste parâmetro é usado. Se false e parâmetro restart não especificado - o serviço é interrompido e reiniciado (mas o systemd usa o comando systemctl restart).
  • status — indica se o script de inicialização do serviço suporta o comando status. Se false, então o valor do parâmetro é usado estado. Padrão true.

exec

Executa comandos externos. Se você não especificar parâmetros cria, somente se, a menos que ou atualizar somente, o comando será executado sempre que o Puppet for executado. Capaz de processar notificações – executa um comando.

opções:

  • nome do recurso - comando a ser executado (opcional)
  • comando — o comando a ser executado (se não estiver especificado no nome)
  • caminho — caminhos para procurar o arquivo executável
  • somente se — se o comando especificado neste parâmetro for concluído com um código de retorno zero, o comando principal será executado
  • a menos que — se o comando especificado neste parâmetro for concluído com um código de retorno diferente de zero, o comando principal será executado
  • cria — se o arquivo especificado neste parâmetro não existir, o comando principal será executado
  • atualizar somente - se um true, então o comando só será executado quando este exec receber notificação de outros recursos
  • cwd — diretório a partir do qual executar o comando
  • usuário — o usuário de quem executar o comando
  • provedor - como executar o comando:
    • posix — um processo filho é simplesmente criado, certifique-se de especificar caminho
    • concha - o comando é lançado no shell /bin/sh, não pode ser especificado caminho, você pode usar globbing, pipes e outros recursos de shell. Geralmente detectado automaticamente se houver algum caractere especial (|, ;, &&, || etc).

cron

Controla cronjobs.

opções:

  • nome do recurso - apenas algum tipo de identificador
  • garantir - estado do trabalho da coroa:
    • present - crie se não existir
    • absent - excluir se existir
  • comando - qual comando executar
  • meio Ambiente — em qual ambiente executar o comando (lista de variáveis ​​de ambiente e seus valores via =)
  • usuário — 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 no crontab será *.

No fantoche 6.0 cron como se retirado da caixa no puppetserver, portanto não há documentação no site geral. Mas ele está na caixa no puppet-agent, portanto não há necessidade de instalá-lo separadamente. Você pode ver a documentação para isso na documentação da quinta versão do PuppetOu no GitHub.

Sobre recursos em geral

Requisitos para exclusividade de recursos

O erro mais comum que encontramos é Declaração duplicada. Este erro ocorre quando dois ou mais recursos do mesmo tipo e com o mesmo nome aparecem no diretório.

Portanto, escreverei novamente: manifestos para o mesmo nó não devem conter recursos do mesmo tipo com o mesmo título!

Às vezes é necessário instalar pacotes com o mesmo nome, mas com gerenciadores de pacotes diferentes. Neste caso, você precisa usar o parâmetro namepara evitar o erro:

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

Outros tipos de recursos têm opções semelhantes para ajudar a evitar duplicação – name у serviço, command у exec, e assim por diante.

Metaparâmetros

Cada tipo de recurso possui alguns parâmetros especiais, independentemente de sua natureza.

Lista completa de metaparâmetros na documentação do Puppet.

Lista curta:

  • requerer — este parâmetro indica de quais recursos este recurso depende.
  • antes - Este parâmetro especifica quais recursos dependem deste recurso.
  • Inscreva-se — este parâmetro especifica de quais recursos este recurso recebe notificações.
  • notificar — Este parâmetro especifica quais recursos recebem notificações deste recurso.

Todos os metaparâmetros listados aceitam um único link de recurso ou uma matriz de links entre colchetes.

Links para recursos

Um link de recurso é simplesmente uma menção ao recurso. Eles são usados ​​principalmente para indicar dependências. Fazer referência a um recurso inexistente causará um erro de compilação.

A sintaxe do link é a seguinte: tipo de recurso com letra maiúscula (se o nome do tipo contiver dois pontos duplos, cada parte do nome entre os dois pontos será maiúscula), então o nome do recurso entre colchetes (o caso do nome não muda!). Não deve haver espaç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

Documentação aqui.

Conforme afirmado anteriormente, as dependências simples entre recursos são transitivas. A propósito, tenha cuidado ao adicionar dependências - você pode 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. As seguintes regras se aplicam às notificações:

  • Se o recurso receber uma notificação, ele será atualizado. As ações de atualização dependem do tipo de recurso - exec executa o comando, serviço reinicia o serviço, pacote reinstala o pacote. Se o recurso não tiver uma ação de atualização definida, nada acontece.
  • Durante uma execução do Puppet, o recurso não é atualizado mais de uma vez. Isso é possível porque as notificações incluem dependências e o gráfico de dependências não contém ciclos.
  • Se o Puppet alterar o estado de um recurso, o recurso enviará notificações para todos os recursos inscritos nele.
  • Se um recurso for atualizado, ele envia notificações a todos os recursos inscritos nele.

Tratamento de parâmetros não especificados

Como regra, se algum parâmetro de recurso não tiver um valor padrão e esse parâmetro não estiver especificado no manifesto, o Puppet não alterará essa propriedade para o recurso correspondente no nó. Por exemplo, se um recurso do tipo lima parâmetro não especificado owner, o Puppet não alterará o proprietário do arquivo correspondente.

Introdução às classes, variáveis ​​e definições

Suponha que temos vários nós que possuem a mesma parte da configuração, mas também existem diferenças - caso contrário, poderíamos descrever tudo em um bloco node {}. Claro, você pode simplesmente copiar partes idênticas da configuração, mas em geral esta é uma solução ruim - a configuração cresce e se você alterar a parte geral da configuração, terá que editar a mesma coisa em vários lugares. Ao mesmo tempo, é fácil cometer um erro e, em geral, o princípio DRY (não se repita) foi inventado por uma razão.

Para resolver este problema existe um design como classe.

Classes

Classe é um bloco nomeado de código poppet. Classes são necessárias para reutilizar código.

Primeiro a classe precisa ser descrita. A descrição em si não adiciona nenhum recurso em lugar nenhum. A classe é descrita em manifestos:

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

Depois disso, a classe pode ser usada:

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

Um 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',
        recure => 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 é nada flexível porque sempre traz a mesma configuração do nginx. Vamos fazer o caminho para a variável de configuração, então esta classe pode ser usada para instalar o nginx com qualquer configuração.

Pode ser feito usando variáveis.

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

Além disso, uma variável só pode ser acessada após ter sido declarada, 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}"

Fantoche tem espaços para nome, e as variáveis, respectivamente, têm área de visibilidade: Uma variável com o mesmo nome pode ser definida em diferentes namespaces. Ao resolver o valor de uma variável, a variável é pesquisada no namespace atual, depois no namespace envolvente e assim por diante.

Exemplos de namespaces:

  • global - variáveis ​​fora da descrição da classe ou do nó vão para lá;
  • namespace do nó na descrição do nó;
  • namespace da classe na descrição da classe.

Para evitar ambiguidade ao acessar uma variável, você pode 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á assim:

class nginx_example {
    package { 'nginx':
        ensure => installed,
    }
    -> file { '/etc/nginx':
        ensure => directory,
        source => $nginx_conf_source,   # здесь используем переменную вместо фиксированной строки
        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
}

Porém, o exemplo dado é ruim porque existe algum “conhecimento secreto” de que em algum lugar dentro da classe é usada uma variável com tal e tal nome. É muito mais correto generalizar esse conhecimento - as classes podem ter parâmetros.

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

O parâmetro pode ser definido como um valor padrão. Se um parâmetro não tiver um valor padrão e o valor não for definido quando usado, causará um erro de compilação.

Vamos parametrizar a classe do exemplo acima e adicionar dois parâmetros: o primeiro, obrigatório, é o caminho para a configuração, e o segundo, opcional, é o nome do pacote com nginx (no Debian, por exemplo, existem 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 digitadas. Comer muitos tipos de dados. Os tipos de dados são normalmente usados ​​para validar valores de parâmetros passados ​​para classes e definições. Se o parâmetro passado não corresponder ao tipo especificado, ocorrerá um erro de compilação.

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

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

Classes: incluem classname vs class{'classname':}

Cada classe é um recurso do tipo classe. Tal como acontece com qualquer outro tipo de recurso, não pode haver duas instâncias da mesma classe no mesmo nó.

Se você tentar adicionar uma classe ao mesmo nó duas vezes usando class { 'classname':} (sem diferença, com parâmetros diferentes ou idênticos), ocorrerá um erro de compilação. Mas se você usar uma classe no estilo de recurso, poderá definir imediatamente todos os seus parâmetros explicitamente no manifesto.

No entanto, se você usar include, a classe poderá ser adicionada quantas vezes desejar. O fato é que include é uma função idempotente que verifica se uma classe foi adicionada ao diretório. Se a classe não estiver no diretório, ele a adiciona, e se já existir, não faz nada. Mas no caso de usar include Você não pode definir parâmetros de classe durante a declaração de classe - todos os parâmetros necessários devem ser definidos em uma fonte de dados externa - Hiera ou ENC. Falaremos sobre eles no próximo artigo.

Define

Como foi dito no bloco anterior, a mesma classe não pode estar presente em um nó mais de uma vez. No entanto, em alguns casos, você precisa usar o mesmo bloco de código com parâmetros diferentes no mesmo nó. Em outras palavras, há necessidade de um tipo de recurso próprio.

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

  1. Instale o pacote com este módulo.
  2. Vamos criar um arquivo de configuração para este módulo.
  3. Criamos um link simbólico para a configuração do php-fpm.
  4. Criamos um link simbólico para a configuração do php cli.

Nesses casos, um projeto como definir (definir, tipo definido, tipo de recurso definido). Um Define é semelhante a uma classe, mas há diferenças: primeiro, cada Define é um tipo de recurso, não um recurso; em segundo lugar, cada definição tem um parâmetro implícito $title, para onde vai o nome do recurso quando ele é declarado. Assim como no caso das classes, primeiro deve ser descrita uma definição, após a qual ela pode ser utilizada.

Um exemplo simplificado com um módulo para PHP:

define php74::module (
  $php_module_name = $title,
  $php_package_name = "php7.4-${title}",
  $version = 'installed',
  $priority = '20',
  $data = "extension=${title}.son",
  $php_module_path = '/etc/php/7.4/mods-available',
) {
  package { $php_package_name:
    ensure          => $version,
    install_options => ['-o', 'DPkg::NoTriggers=true'],  # триггеры дебиановских php-пакетов сами создают симлинки и перезапускают сервис php-fpm - нам это не нужно, так как и симлинками, и сервисом мы управляем с помощью Puppet
  }
  -> file { "${php_module_path}/${php_module_name}.ini":
    ensure  => $ensure,
    content => $data,
  }
  file { "/etc/php/7.4/cli/conf.d/${priority}-${php_module_name}.ini":
    ensure  => link,
    target  => "${php_module_path}/${php_module_name}.ini",
  }
  file { "/etc/php/7.4/fpm/conf.d/${priority}-${php_module_name}.ini":
    ensure  => link,
    target  => "${php_module_path}/${php_module_name}.ini",
  }
}

node server3.testdomain {
  php74::module { 'sqlite3': }
  php74::module { 'amqp': php_package_name => 'php-amqp' }
  php74::module { 'msgpack': priority => '10' }
}

A maneira mais fácil de detectar o erro de declaração duplicada é em Definir. Isso acontece se houver um recurso com nome constante na definição e houver duas ou mais instâncias dessa definição em algum nó.

É fácil se proteger disso: todos os recursos dentro da definição devem ter um nome dependendo $title. Uma alternativa é a adição idempotente de recursos; no caso mais simples, basta mover os recursos comuns a todas as instâncias da definição para uma classe separada e incluir esta classe na definição - função include idempotente.

Existem outras formas de obter idempotência ao adicionar recursos, nomeadamente através de funções defined и ensure_resources, mas contarei a vocês sobre isso no próximo episódio.

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

Classes e definições adicionam as seguintes regras para lidar com dependências e notificações:

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

Declarações condicionais e seletores

Documentação aqui.

if

Tudo é simples aqui:

if ВЫРАЖЕНИЕ1 {
  ...
} elsif ВЫРАЖЕНИЕ2 {
  ...
} else {
  ...
}

a menos que

a menos que seja um if ao contrário: o bloco de código será executado se a expressão for falsa.

unless ВЫРАЖЕНИЕ {
  ...
}

casas

Também não há nada complicado aqui. Você pode usar valores regulares (strings, números, etc.), expressões regulares e tipos de dados como valores.

case ВЫРАЖЕНИЕ {
  ЗНАЧЕНИЕ1: { ... }
  ЗНАЧЕНИЕ2, ЗНАЧЕНИЕ3: { ... }
  default: { ... }
}

Seletores

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

$var = $othervar ? { 'val1' => 1, 'val2' => 2, default => 3 }

Módulos

Quando a configuração é pequena, ela pode ser facilmente mantida em um manifesto. Mas quanto mais configurações descrevemos, mais classes e nós existem no manifesto, ele cresce e se torna inconveniente trabalhar com eles.

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

Módulos - são conjuntos de classes, definições e outras entidades Puppet colocadas em um diretório separado. Em outras palavras, um módulo é uma peça independente da lógica Puppet. Por exemplo, pode haver um módulo para trabalhar com nginx, e ele conterá o que e somente o que é necessário para trabalhar com nginx, ou pode haver um módulo para trabalhar com PHP e assim por diante.

Os módulos são versionados e as dependências dos módulos entre si também são suportadas. Existe um repositório aberto de módulos - Forja de Marionetes.

No servidor fantoche, os módulos estão localizados no subdiretório de módulos do diretório raiz. Dentro de cada módulo existe um esquema de diretório padrão - manifestos, arquivos, modelos, lib e assim por diante.

Estrutura de arquivos em um módulo

A raiz do módulo pode conter os seguintes diretórios com nomes descritivos:

  • manifests - contém manifestos
  • files - contém arquivos
  • templates - contém modelos
  • lib - contém código Ruby

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

Nomes de recursos e nomes de arquivos no módulo

Documentação aqui.

Os recursos (classes, definições) em um módulo não podem ter o nome que você quiser. Além disso, existe uma correspondência direta entre o nome de um recurso e o nome do arquivo no qual o Puppet irá procurar uma descrição desse recurso. Se você violar as regras de nomenclatura, o Puppet simplesmente não encontrará a descrição do recurso e você receberá 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 for chamado foo, então todos os recursos nele devem ser nomeados foo::<anything>ou apenas foo.
  • O recurso com o nome do módulo deve estar no arquivo init.pp.
  • Para outros recursos, o esquema de nomenclatura de arquivos é o seguinte:
    • o prefixo com o nome do módulo é descartado
    • todos os dois pontos duplos, se houver, são substituídos por barras
    • extensão é adicionada .pp

Vou demonstrar com um exemplo. Digamos que estou escrevendo um módulo nginx. Ele contém os seguintes recursos:

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

templates

Certamente você mesmo sabe o que são modelos, não vou descrevê-los em detalhes aqui. Mas vou deixar isso por precaução link para a Wikipédia.

Como usar modelos: O significado de um modelo pode ser expandido usando uma função template, que recebe o caminho para o modelo. Para recursos do tipo lima usado junto com o parâmetro content. Por exemplo, assim:

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

Ver caminho <modulename>/<filename> implica arquivo <rootdir>/modules/<modulename>/templates/<filename>.

Além disso, há uma função inline_template — recebe o texto do modelo como entrada, não o nome do arquivo.

Nos modelos, você pode usar todas as variáveis ​​do Puppet no escopo atual.

O Puppet oferece suporte a modelos nos formatos ERB e EPP:

Resumidamente sobre ERB

Estruturas de controle:

  • <%= ВЫРАЖЕНИЕ %> — insira o valor da expressão
  • <% ВЫРАЖЕНИЕ %> — calcula o valor de uma expressão (sem inseri-la). Declarações condicionais (se) e loops (cada) geralmente vão aqui.
  • <%# КОММЕНТАРИЙ %>

Expressões em ERB são escritas em Ruby (ERB é na verdade Embedded Ruby).

Para acessar variáveis ​​do manifesto, você precisa adicionar @ ao nome da variável. Para remover uma quebra de linha que aparece após uma construção de controle, você precisa usar uma tag de fechamento -%>.

Exemplo de uso do modelo

Digamos que estou escrevendo um módulo para controlar o ZooKeeper. A classe responsável por criar a configuração é 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 modelo correspondente zoo.cfg.erb - Então:

<% if @nodes.length > 0 -%>
<% @nodes.each do |node, id| -%>
server.<%= id %>=<%= node %>:<%= @port_leader %>:<%= @port_quorum %>;<%= @port_client %>
<% end -%>
<% end -%>

dataDir=<%= @datadir %>

<% @properties.each do |k, v| -%>
<%= k %>=<%= v %>
<% end -%>

Fatos e variáveis ​​incorporadas

Muitas vezes, a parte específica da configuração depende do que está acontecendo atualmente no nó. Por exemplo, dependendo de qual é a versão do Debian, você precisa instalar uma ou outra versão do pacote. Você pode monitorar tudo isso manualmente, reescrevendo os manifestos se os nós mudarem. Mas esta não é uma abordagem séria; a automação é muito melhor.

Para obter informações sobre os nós, o Puppet possui um mecanismo chamado fatos. Fatos - são informações sobre o nó, disponíveis em manifestos na forma de variáveis ​​​​comuns no namespace global. Por exemplo, nome do host, versão do sistema operacional, arquitetura do processador, lista de usuários, lista de interfaces de rede e seus endereços e muito, muito mais. Os fatos estão disponíveis em manifestos e modelos como variáveis ​​regulares.

Um exemplo de trabalho com fatos:

notify { "Running OS ${facts['os']['name']} version ${facts['os']['release']['full']}": }
# ресурс типа notify просто выводит сообщение в лог

Formalmente falando, um fato possui um nome (string) e um valor (vários tipos estão disponíveis: strings, arrays, dicionários). Comer conjunto de fatos embutidos. Você também pode escrever o seu próprio. Coletores de fatos são descritos como funções em Rubyou como arquivos executáveis. Os fatos também podem ser apresentados na forma arquivos de texto com dados nos nós.

Durante a operação, o agente fantoche primeiro copia todos os coletores de fatos disponíveis do pappetserver para o nó, após o que os inicia e envia os fatos coletados para o servidor; Depois disso, o servidor começa a compilar o catálogo.

Fatos na forma de arquivos executáveis

Tais fatos são colocados em módulos no diretório facts.d. Claro, os arquivos devem ser executáveis. Quando executados, eles devem enviar informações para a saída padrão no formato YAML ou chave=valor.

Não esqueça que os fatos se aplicam a todos os nós controlados pelo servidor poppet no qual seu módulo está implantado. Portanto, no script, tome cuidado para verificar se o sistema possui todos os programas e arquivos necessários para o seu fato funcionar.

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

Fatos sobre rubi

Tais fatos são colocados em 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 de texto

Tais fatos são colocados em nós do diretório /etc/facter/facts.d no antigo fantoche ou /etc/puppetlabs/facts.d no novo fantoche.

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

Chegando aos fatos

Existem duas maneiras de abordar os fatos:

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

É melhor usar um dicionário $facts, ou melhor ainda, indique o namespace global ($::facts).

Aqui está a seção relevante da documentação.

Variáveis ​​incorporadas

Além dos fatos, há também algumas variáveis, disponível no namespace global.

  • fatos confiáveis — variáveis ​​que são retiradas do certificado do cliente (já que o certificado geralmente é emitido em um servidor poppet, o agente não pode simplesmente pegar e alterar seu certificado, então as variáveis ​​são “confiáveis”): o nome do certificado, o nome do host e domínio, extensões do certificado.
  • fatos do servidor —variáveis ​​relacionadas às informações sobre o servidor—versão, nome, endereço IP do servidor, ambiente.
  • fatos do agente — variáveis ​​adicionadas diretamente pelo agente fantoche, e não pelo fator — nome do certificado, versão do agente, versão do fantoche.
  • variáveis ​​mestras - Variáveis ​​Pappetmaster (sic!). É quase 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 diferem em cada escopo: o nome do módulo atual e o nome do módulo no qual o objeto atual foi acessado. Eles podem ser usados, por exemplo, para verificar se suas aulas particulares não estão sendo utilizadas diretamente de outros módulos.

Adição 1: como executar e depurar tudo isso?

O artigo continha muitos exemplos de código fantoche, mas não nos dizia como executar esse código. Bem, estou me corrigindo.

Um agente é suficiente para executar o Puppet, mas na maioria dos casos você também precisará de um servidor.

Agente

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

No caso mais simples, para utilizar a configuração do fantoche, basta iniciar o agente em modo serverless: desde que o código do fantoche seja copiado para o nó, execute puppet apply <путь к манифесту>:

atikhonov@atikhonov ~/puppet-test $ cat helloworld.pp 
node default {
    notify { 'Hello world!': }
}
atikhonov@atikhonov ~/puppet-test $ puppet apply helloworld.pp 
Notice: Compiled catalog for atikhonov.localdomain in environment production in 0.01 seconds
Notice: Hello world!
Notice: /Stage[main]/Main/Node[default]/Notify[Hello world!]/message: defined 'message' as 'Hello world!'
Notice: Applied catalog in 0.01 seconds

É melhor, é claro, configurar o servidor e executar agentes nos nós no modo daemon - então, uma vez a cada meia hora, eles aplicarão a configuração baixada do servidor.

Você pode imitar o modelo push de trabalho - vá até o nó de seu interesse e comece sudo puppet agent -t. Chave -t (--test) na verdade inclui várias opções que podem ser habilitadas individualmente. Essas opções incluem o seguinte:

  • não execute em modo daemon (por padrão o agente inicia em modo daemon);
  • desligar após aplicar o catálogo (por padrão, o agente continuará trabalhando e aplicando a configuração uma vez a cada meia hora);
  • escreva um registro de trabalho detalhado;
  • mostrar alterações nos arquivos.

O agente possui um modo de operação inalterado - você pode usá-lo quando não tiver certeza de ter escrito a configuração correta e quiser verificar exatamente o que o agente mudará durante a operação. Este modo é habilitado pelo parâmetro --noop na linha de comando: sudo puppet agent -t --noop.

Além disso, você pode habilitar o log de depuração do trabalho - nele o puppet escreve sobre todas as ações que realiza: sobre o recurso que está processando no momento, sobre os parâmetros deste recurso, sobre quais programas ele inicia. Claro que este é um parâmetro --debug.

Servidor

Não considerarei a configuração completa do pappetserver e a implantação do código nele neste artigo; direi apenas que vem pronto para uso uma versão totalmente funcional do servidor que não requer configuração adicional para funcionar com um pequeno número de nós (digamos, até cem). Um número maior de nós exigirá ajuste - por padrão, o puppetserver não executa mais do que quatro trabalhadores, para maior desempenho você precisa aumentar seu número e não se esqueça de aumentar os limites de memória, caso contrário o servidor irá coletar lixo na maior parte do tempo.

Implantação de código - se você precisar de forma rápida e fácil, dê uma olhada (em r10k)[https://github.com/puppetlabs/r10k], para instalações pequenas deve ser suficiente.

Adendo 2: Diretrizes de Codificação

  1. Coloque toda a lógica em classes e definições.
  2. Mantenha classes e definições em módulos, não em manifestos que descrevem nós.
  3. Use os fatos.
  4. Não faça ifs com base em nomes de host.
  5. Sinta-se à vontade para adicionar parâmetros para classes e definições - isso é melhor do que a lógica implícita oculta no corpo da classe/definição.

Explicarei por que recomendo fazer isso no próximo artigo.

Conclusão

Vamos terminar com a introdução. No próximo artigo falarei sobre Hiera, ENC e PuppetDB.

Apenas usuários registrados podem participar da pesquisa. Entrarpor favor

Na verdade, há muito mais material - posso escrever artigos sobre os seguintes tópicos, votar no que você teria interesse em ler:

  • 59,1%Construções avançadas de fantoches - algumas merdas de próximo nível: loops, mapeamento e outras expressões lambda, coletores de recursos, recursos exportados e comunicação entre hosts via Puppet, tags, provedores, tipos de dados abstratos.13
  • 31,8%“Eu sou o administrador da minha mãe” ou como nós no Avito fizemos amizade com vários servidores poppet de diferentes versões e, em princípio, a parte sobre administração do servidor poppet.7
  • 81,8%Como escrevemos código fantoche: instrumentação, documentação, testes, CI/CD.18

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

Fonte: habr.com