Gerenciamento de Configuração 101: Escrevendo Receitas do Chef

Introdução

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

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

Esta parte da série o guiará pelo processo de automatização do provisionamento de servidores usando o Chef, uma poderosa ferramenta de gerenciamento de configuração que aproveita a linguagem de programação Ruby para automatizar a administração e o provisionamento da infraestrutura. Vamos nos concentrar na terminologia da linguagem, sintaxe e recursos necessários para criar um exemplo simplificado para automatizar completamente o deployment de um servidor web Ubuntu 18.04 usando o Apache.

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

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

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

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

Começando

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

Termos do Chef

  • Chef Server: um servidor central que armazena informações e gerencia o provisionamento dos nodes
  • Chef Node: um servidor individual que é gerenciado por um Chef Server
  • Chef Workstation: uma máquina controladora na qual os provisionamentos são criados e carregados para o Chef Server
  • Recipe (receita): um arquivo que contém um conjunto de instruções (recursos) a serem executados. Uma receita deve estar contida dentro de um Cookbook
  • Resource (recurso): uma parte do código que declara um elemento do sistema e que ação deve ser executada. Por exemplo, para instalar um pacote, declaramos um recurso package com a ação install
  • Cookbook (livro de receitas): uma coleção de receitas e outros arquivos relacionados organizados de forma predefinida para facilitar o compartilhamento e a reutilização de partes de um provisionamento
  • Attributes (atributos): detalhes sobre um node específico. Os Attributes (atributos) podem ser automáticos (veja a próxima definição) e também podem ser definidos nas receitas
  • Automatic Attributes: variáveis globais que contêm informações sobre o sistema, como interfaces de rede e sistema operacional (conhecidos como facts em outras ferramentas). Esses atributos automáticos são coletados por uma ferramenta chamada Ohai
  • Services: usado para acionar alterações de status do serviço, como reiniciar ou parar um serviço

Formato da Receita

As receitas do Chef são escritas usando Ruby. Uma receita é basicamente uma coleção de definições de recursos que criarão um conjunto passo a passo de instruções a serem executadas pelos nodes. Essas definições de recursos podem ser combinadas com o código Ruby para maior flexibilidade e modularidade.

Abaixo você pode encontrar um exemplo simples de uma receita que irá executar o apt-get update e instalar o vim posteriormente:

execute "apt-get update" do  command "apt-get update" end  apt_package "vim" do  action :install end  

Escrevendo Receitas

Trabalhando com Variáveis

As variáveis locais podem ser definidas nas receitas como variáveis locais regulares do Ruby. O exemplo abaixo mostra como criar uma variável local que é usada posteriormente dentro de uma definição de recurso:

package  = "vim"  apt_package package do  action :install end 

Essas variáveis, no entanto, têm um escopo limitado, sendo válidas apenas dentro do arquivo em que foram definidas. Se você deseja criar uma variável e disponibilizá-la globalmente, para poder usá-la em qualquer um dos seus cookbooks ou receitas, você precisa definir um atributo personalizado.

Usando Atributos

Os atributos representam detalhes sobre um node. O Chef possui atributos automáticos, que são os atributos coletados por uma ferramenta chamada Ohai e contêm informações sobre o sistema (como plataforma, nome do host e endereço IP padrão), mas também permitem definir seus próprios atributos personalizados.

Os atributos têm níveis de precedência diferentes, definidos pelo tipo de atributo que você cria. Os atributos default são a escolha mais comum, pois ainda podem ser sobrescritos por outros tipos de atributos quando desejado.

O exemplo a seguir mostra como o exemplo anterior seria com um atributo de node default em vez de uma variável local:

node.default['main']['package'] = "vim"  apt_package node['main']['package'] do  action :install end 

Há dois detalhes a serem observados neste exemplo:

A prática recomendada ao definir variáveis de node é organizá-las como hashes usando o cookbook atual em uso como chave. Nesse caso, usamos main, porque temos um cookbook com o mesmo nome. Isso evita confusão se você estiver trabalhando com vários cookbooks que podem ter atributos com nomes semelhantes.
Observe que usamos node.default ao definir o atributo, mas ao acessar seu valor posteriormente, usamos node diretamente. O uso do node.default define que estamos criando um atributo do tipo default. Esse atributo pode ter seu valor sobrescrito por outro tipo com precedência mais alta, como atributos normal ou override.

A precedência dos atributos pode ser um pouco confusa no início, mas você se acostumará com isso depois de alguma prática. Para ilustrar o comportamento, considere o seguinte exemplo:

node.normal['main']['package']  = "vim"  node.override['main']['package'] = "git"  node.default['main']['package'] = "curl"  apt_package node['main']['package'] do  action :install end 

Você sabe qual pacote será instalado neste caso? Se você achou que é o git, adivinhou corretamente. Independentemente da ordem em que os atributos foram definidos, a maior precedência do tipo override fará com que o node['main']['package'] seja avaliado como git.

Usando Loops

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

O Chef suporta todas as estruturas de loop Ruby para criar loops dentro de receitas. Para uso simples, each é uma escolha comum:

['vim', 'git', 'curl'].each do |package|  apt_package package do    action :install  end end 

Em vez de usar um array inline, você também pode criar uma variável ou atributo para definir os parâmetros que deseja usar dentro do loop. Isso manterá as coisas mais organizadas e fáceis de ler. Abaixo, o mesmo exemplo agora usando uma variável local para definir os pacotes que devem ser instalados:

packages = ['vim', 'git', 'curl']  packages.each do |package|  apt_package package do    action :install  end end 

Usando Condicionais

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

O Chef suporta todos as condicionais do Ruby para criar instruções condicionais nas receitas. Além disso, todos os tipos de recursos suportam duas propriedades especiais que avaliarão uma expressão antes de decidir se a tarefa deve ser executada ou não: if_only e not_if.

O exemplo abaixo verificará a existência do php antes de tentar instalar a extensão php-pear. Ele usará o comando which para verificar se existe um executável php atualmente instalado neste sistema. Se o comando which php retornar falso, esta tarefa não será executada:

apt_package "php-pear" do  action :install  only_if "which php" end 

Se quisermos fazer o oposto, executando um comando o tempo todo exceto quando uma condição é avaliada como verdadeira, usamos not_if. Este exemplo instalará o php5, a menos que o sistema seja o CentOS:

apt_package "php5" do  action :install  not_if { node['platform'] == 'centos' } end 

Para realizar avaliações mais complexas, ou se você desejar executar várias tarefas sob uma condição específica, você pode usar qualquer uma das condicionais padrão do Ruby. O exemplo a seguir só executará apt-get update quando o sistema for Debian ou Ubuntu:

if node['platform'] == 'debian' || node['platform'] == 'ubuntu'  execute "apt-get update" do    command "apt-get update"  end end 

O atributo node['platform'] é um atributo automático do Chef. O último exemplo foi apenas para demonstrar uma construção condicional mais complexa, no entanto, pode ser substituída por um teste simples usando o atributo automático node['platform_family'], que retornaria “debian” para os sistemas Debian e Ubuntu.

Trabalhando com Templates

Os templates geralmente são usados para definir arquivos de configuração, permitindo o uso de variáveis e outros recursos destinados a tornar esses arquivos mais versáteis e reutilizáveis.

O Chef usa templates Embedded Ruby (ERB), que é o mesmo formato usado pelo Puppet. Eles suportam condicionais, loops e outros recursos do Ruby.

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

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

Para aplicar o template, precisamos criar um recurso template. É assim que você aplicaria esse template para substituir o virtual host padrão do Apache:

template "/etc/apache2/sites-available/000-default.conf" do  source "vhost.erb"  variables({ :doc_root => node['main']['doc_root'] })  action :create end  

O Chef faz algumas suposições ao lidar com arquivos locais, a fim de reforçar a organização e a modularidade. Nesse caso, o Chef procuraria um arquivo de template vhost.erb dentro de uma pasta templates que deveria estar no mesmo cookbook em que esta receita está localizada.

Ao contrário das outras ferramentas de gerenciamento de configuração que vimos até agora, o Chef tem um escopo mais estrito para variáveis. Isso significa que você precisará fornecer explicitamente todas as variáveis que planeja usar dentro de um template, ao definir o recurso template. Neste exemplo, usamos o método variables para passar adiante o atributo doc_root que precisamos no modelo de virtual host.

Definindo e Acionando Serviços

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

No Chef, os recursos de serviço precisam ser declarados antes de você tentar notificá-los, caso contrário, você receberá um erro.

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

service "apache2" do   action [ :enable, :start ] end 

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

template "/etc/apache2/sites-available/000-default.conf" do  source "vhost.erb"  variables({ :doc_root => node['main']['doc_root'] })  action :create  notifies :restart, resources(:service => "apache2") end 

Exemplo de Receita

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

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

Abaixo você pode encontrar a receita completa:

  • node.default['main']['doc_root'] = "/vagrant/web"
  • execute "apt-get update" do
  • command "apt-get update"
  • end
  • apt_package "apache2" do
  • action :install
  • end
  • service "apache2" do
  • action [ :enable, :start ]
  • end
  • directory node['main']['doc_root'] do
  • owner 'www-data'
  • group 'www-data'
  • mode '0644'
  • action :create
  • end
  • cookbook_file "#{node['main']['doc_root']}/index.html" do
  • source 'index.html'
  • owner 'www-data'
  • group 'www-data'
  • action :create
  • end
  • template "/etc/apache2/sites-available/000-default.conf" do
  • source "vhost.erb"
  • variables({ :doc_root => node['main']['doc_root'] })
  • action :create
  • notifies :restart, resources(:service => "apache2")
  • end

Receita Explicada

linha 1

A receita começa com uma definição de atributo, node['main']['doc_root']. Poderíamos ter usado uma variável local simples aqui, no entanto, na maioria dos cenários de casos de uso, as receitas precisam definir variáveis globais que serão usadas nas receitas incluídas ou em outros arquivos. Para essas situações, é necessário criar um atributo em vez de uma variável local, pois a última possui um escopo limitado.

linhas 3-5

Esse recurso execute executa um apt-get update.

linhas 7-10

Este recurso apt_package instala o pacote apache2.

linhas 12-15

Este recurso service ativa e inicia o serviço apache2. Posteriormente, precisaremos notificar esse recurso para reiniciar o serviço. É importante que a definição de serviço venha antes de qualquer recurso que tente notificar um serviço, caso contrário, você receberá um erro.

linhas 17-22

Este recurso directory usa o valor definido pelo atributo personalizado node['main']['doc_root'] para criar um diretório que servirá como nosso document root

linhas 24-29

Um recurso cookbook_file é usado para copiar um arquivo local em um servidor remoto. Este recurso copiará nosso arquivo index.html e o colocará dentro do document root que criamos em uma tarefa anterior.

linhas 31-36

Por fim, esse recurso template aplica nosso template de virtual host do Apache e notifica o serviço apache2 para uma reinicialização.

Conclusão

O Chef é uma poderosa ferramenta de gerenciamento de configuração que utiliza a linguagem Ruby para automatizar o provisionamento e o deployment de servidores. Ele lhe oferece liberdade para usar os recursos de linguagem padrão para obter o máximo de flexibilidade, além de oferecer DSLs personalizadas para alguns recursos.