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.
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
- 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
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 arquivopresent
— deve haver um arquivo de qualquer tipo (se não houver arquivo, será criado um arquivo normal)file
- arquivo normaldirectory
- diretóriolink
- 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 esquemahttp:
(espero que esteja claro o que acontecerá neste caso), e mesmo com o diagramafile:
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 instaladalatest
- última versão instaladaabsent
- 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ãofalse
.
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çadostopped
- 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. Sefalse
e parâmetro restart não especificado - o serviço é interrompido e reiniciado (mas o systemd usa o comandosystemctl restart
). - status — indica se o script de inicialização do serviço suporta o comando
status
. Sefalse
, então o valor do parâmetro é usado estado. Padrãotrue
.
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 existirabsent
- 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
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 name
para 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
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
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
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
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
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:
- Instale o pacote com este módulo.
- Vamos criar um arquivo de configuração para este módulo.
- Criamos um link simbólico para a configuração do php-fpm.
- Criamos um link simbólico para a configuração do php cli.
Nesses casos, um projeto como $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
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 -
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 manifestosfiles
- contém arquivostemplates
- contém modeloslib
- 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
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 nomeadosfoo::<anything>
ou apenasfoo
. - 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 manifestoinit.pp
; - classe
nginx::service
descrito no manifestoservice.pp
; - definir
nginx::server
descrito no manifestoserver.pp
; - definir
nginx::server::location
descrito no manifestoserver/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
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
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
).
Variáveis incorporadas
Além dos fatos, há também
- 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
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)[
Adendo 2: Diretrizes de Codificação
- Coloque toda a lógica em classes e definições.
- Mantenha classes e definições em módulos, não em manifestos que descrevem nós.
- Use os fatos.
- Não faça ifs com base em nomes de host.
- 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.
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