SaltStack com Pillars e Grains
Pillar
O Pillar no SaltStack é uma estrutura usada para armazenar dados sensíveis e específicos de Minions, como senhas, chaves criptográficas, configurações específicas e outros tipos de informações confidenciais. Esses dados são fornecidos pelo Salt Master e são gerenciados de forma centralizada e segura. Diferente dos grains, que fornecem informações sobre o estado do sistema operacional dos Minions e são gerados automaticamente, os pillars são definidos manualmente no Salt Master. Eles são altamente configuráveis e podem ser adaptados para cada Minion individualmente ou para grupos de Minions. Isso é útil em situações em que você precisa aplicar dados sensíveis ou configurações únicas de maneira controlada.
Imagine que você precise fornecer uma senha específica para um Minion. Em vez de armazenar essa senha diretamente em arquivos de estado (que poderiam ser expostos), você a define como um Pillar no Master, garantindo que apenas o Minion específico tenha acesso a ela. Vamos começar configurando onde o Salt Master deve procurar pelo Pillar (após modificar o arquivo abaixo reinicie o Salt Master):
pillar_roots:
base:
- /srv/salt/pillar
Agora vamos criar esse diretório e começar a configurar ele.
# Crie o diretório:
sudo mkdir /srv/salt/pillar
# Agora crie o arquivo 'top.sls' do pillar:
sudo touch /srv/salt/pillar/top.sls
# Agora crie o arquivo 'top.sls' do pillar:
sudo touch /srv/salt/pillar/top.sls
O arquivo
top.sls
do Pillar tem a mesma estrutura doStates
e funciona da mesma maneira.
Vamos começar criando uma estrutura simples e que seja fácil de entender.
base:
'*':
- hosts
Agora temos que criar o diretório hosts
e configurar o init.sls
dentro dele.
# Crie o diretório hosts:
sudo mkdir /srv/salt/pillar/hosts
Agora vamos configurar de fato os dados do Pillar.
servidores:
dedsec:
interface: eth0
address: 192.168.121.55/24
zion:
interface: eth0
address: 192.168.121.161/24
Ao incluir e/ou modificar alguma coisa no Pillar, podemos executar o seguinte comando ao invés de reiniciar o salt-master: salt '*' saltutil.refresh_pillar
. Agora já podemos usar alguns comendos específicos do Pillar.
# lista todas as chaves disponíveis no Pillar (servidores é a chave inicial):
$ salt '*' pillar.ls
zion:
- servidores
dedsec:
- servidores
# Exibe todas as chaves e seus valores associados no Pillar:
$ salt 'zion' pillar.items
zion:
----------
servidores:
----------
dedsec:
----------
address:
192.168.121.55/24
interface:
eth0
zion:
----------
address:
192.168.121.161/24
interface:
eth0
# Exibe o valor de uma chave específica
$ salt 'zion' pillar.item servidores:zion:address
zion:
----------
servidores:zion:address:
192.168.121.161/24
# Retorna o valor de uma chave específica, mas com a vantagem de permitir
# definir um valor padrão caso a chave solicitada não exista.
$ salt '*' pillar.get 'service/port' 80
dedsec:
80
zion:
80
# Retorna os dados brutos do Pillar disponíveis para um Minion
$ salt 'zion' pillar.data
zion:
----------
servidores:
----------
dedsec:
----------
address:
192.168.121.55/24
interface:
eth0
zion:
----------
address:
192.168.121.161/24
interface:
eth0
O arquivo init.sls
pode ser criado de forma mais simples. Só criei a chave servidores
para demonstrar mesmo.
dedsec:
interface: eth0
address: 192.168.121.55/24
zion:
interface: eth0
address: 192.168.121.161/24
Agora os comandos ficam mais simples também:
# Retorna os dados brutos do Pillar disponíveis para um Minion
$ salt 'zion' pillar.data
zion:
----------
dedsec:
----------
address:
192.168.121.55/24
interface:
eth0
zion:
----------
address:
192.168.121.161/24
interface:
eth0
# lista todas as chaves disponíveis no Pillar:
$ salt 'zion' pillar.ls
zion:
- dedsec
- zion
# Exibe o valor de uma chave específica
$ salt 'zion' pillar.item zion:address
zion:
----------
zion:address:
192.168.121.161/24
Se você quiser mudar o Pillar enquanto executa comandos Ad-Hoc utilize o parâmetro pillarenv
como em:
# Mudar 'pillar' no salt:
salt '*' state.apply mystates pillarenv=testing
# Mudar 'file_roots' para ser outro ambiente e não o 'Base' no salt:
salt '*' state.apply mystates saltenv=prod
States com Jinja2 e Pillar
O Salt é capaz de usar templates que aproveitam os Grains e Pillars para tornar o sistema de estados (STATES) mais dinâmico. Além do Jinja2, que é o mecanismo de template mais comum, o Salt também suporta outros engines de template. A partir da versão 2015.5, os seguintes motores estão disponíveis:
- jinja
- mako
- wempy
- cheetah
- genshi
Esses motores estão disponíveis através do sistema de rendering do Salt, que permite a criação de templates para arquivos de configuração ou outros tipos de dados. Além disso, o Salt oferece outros renderizadores projetados principalmente para descrever estruturas de dados, como:
- yaml (o formato padrão para SLS)
- yamlex
- json
- msgpack
- py (Python)
- pyobjects
- pydsl
- gpg
O renderizador gpg é capaz de descriptografar dados GPG armazenados no Master antes de passá-los por outro renderizador. Por padrão, os arquivos SLS (Salt State) passam primeiro pelo renderizador Jinja para aplicar lógicas dinâmicas e variáveis, e em seguida são processados pelo renderizador yaml, que organiza os dados em uma estrutura legível e aplicável pelos Minions.
Com esses diferentes renderizadores, o Salt permite uma flexibilidade para a criação de templates dinâmicos e a automação de configuração de sistemas de maneira robusta e segura. Agora vamos ver como integrar o State do Salt juntamente com o Pillar e como utilizar uma das nossas maiores ferramentas, o Jinja2.
Jinja2
Jinja2 é uma linguagem de template para Python. Um template fornece um método para gerar conteúdo dinamicamente. Existem dois tipos principais de sintaxe Jinja2 usados no Salt. A primeira é a variável, que usa chaves duplas e que é mostrada no código a abaixo:
{{ foo }}
{{ foo.bar }}
{{ foo['bar'] }}
{{ get_data() }}
Para esses exemplos, o conteúdo da variável referenciada ou os resultados da chamada de função são colocados no documento no local do bloco Jinja2, ou seja, quando você usa uma variável ou uma função em um bloco Jinja2, o valor dessa variável ou o resultado da função substitui o próprio código Jinja no momento em que o template é processado, tornando possível a criação de templates dinamicos.
Quando você usa uma variável dentro de {{ }}
no Jinja2, ela será substituída pelo valor que essa variável contém. Por exemplo, se você tiver {{ foo }}
e a variável foo
tiver o valor Olá, mundo!
, então, no documento final, o código {{ foo }}
será substituído por Olá, mundo!
. O Jinja2 também tem acesso a instruções de controle básicas. Os blocos de instrução de controle usa uma chave e um sinal de porcentagem, que é representado no código a seguir: {% %}
.
Aqui está um exemplo de um bloco condicional:
{% if myvar == 'foo' %}
somecontent
{% elif myvar == 'bar' %}
othercontent
{% else %}
morecontent
{% endif %}
Aqui está um exemplo de loop:
{% for user in ['larry', 'moe', 'curly'] %}
É o ususário {{ user }}!
Olá {{ user }}!
{% endfor %}
Também podemos definir variáveis para uso posterior no modelo, como segue:
{% set myvar = 'foo'%}
Com essas noções básicas de sintaxe, estamos prontos para usar Jinja2 no Salt!
Aplicando Jinja2 no state
Vamos ver um exemplo de aplicação do Jinja2, vamos pegar como exemplo o web server Apache, em ambientes Redhat ele se chama httpd
, já em ambientes Debian ele se chama apache2
, então vamos montar um sls
para fazer a instalação do Apache independentemente do sistema ser Debian ou Redhat.
install_apache:
pkg.installed:
{% if grains['os_family'] == 'Debian' %}
- name: apache2
{% elif grains['os_family'] == 'RedHat' %}
- name: httpd
{% endif %}
Lembre-se que com o Grains podemos obter informações do Sistema do minion. O os_family
nos informa a familia da distro que o minion pertence.
Criando um template
O SaltStack não possui um módulo nativo para gerenciar o Netplan, o gerenciador de configurações de rede nos servidores Ubuntu. Embora seja possível desenvolver um módulo personalizado em Python para o Salt, uma solução mais prática e direta é criar um template utilizando Jinja. Além disso, essa abordagem pode ser otimizada com o uso de Grains, permitindo uma maior flexibilidade e personalização das configurações de rede.
Vamos iniciar configurando nosso Pillar para gerar um template de configuração de rede:
dedsec:
interface1: eth0
address1: 192.168.121.161/24
interface2: eth1
address2: 192.168.100.4/24
nameserver1: 8.8.8.8
nameserver2: 8.8.4.4
renderer: networkd
zion:
interface1: eth0
address1: 192.168.121.55/24
interface2: eth1
address2: 192.168.100.3/24
nameserver1: 9.9.9.9
nameserver2: 8.8.8.8
renderer: networkd
Em seguida, configure o arquivo top do Pillar para saber o que deve ser executado quando o Pillar for executado sem nenhum parâmetro que informe exatamente qual dados do Pillar deve ser executado.
base:
'G@os:Ubuntu':
- ubuntu_network
Com os dados disponíveis no nosso Pillar, podemos utilizá-los para configurar a rede em cada Minion. Agora, vamos criar o SLS que vai de fato aplicar essas configurações. Primeiro, crie os diretórios necessários para armazenar os templates:
# Crie o diretório onde ficará o SLS:
sudo mkdir /srv/salt/network
# Crie um diretório dentro dele para armazenar apenas os templates que serão
# enviados para os minions:
sudo mkdir /srv/salt/network/files
Agora configure o arquivo top. Quanto nenhum SLS for passado como parâmetro o top.sls
vai seguir a ordem abaixo.
base:
'*':
- network
Em seguida, edite o init.sls
dentro do diretório network
, que irá definir as ações que o Salt vai executar, como mover o arquivo de template para o Minion e aplicar as configurações usando Jinja e os dados do Pillar:
/etc/netplan/:
file.directory:
- clean: True
/etc/netplan/01-netcfg.yaml:
file.managed:
- source: salt://network/files/01-netcfg.yaml
- template: jinja
- user: root
- group: root
- mode: 644
O file.directory
remove qualquer arquivo de configuração de rede existente em /etc/netplan/
.
O segundo bloco usa file.managed
, move o template 01-netcfg.yaml
do diretório network/files
no Salt Master para o Minion, renderizando o template com Jinja para aplicar as variáveis do Pillar.
Antes de aplicar o template, vamos criar um arquivo base que centraliza o acesso aos dados do Pillar. Isso evita duplicação de código em outros SLS que precisem das mesmas informações:
{%- set gget = salt['grains.get'] %}
{%- set pget = salt['pillar.get'] %}
{%- set minionName = gget('host') %}
{%- set base = minionName ~ ':' %}
{%- set interface1 = pget(base ~ 'interface1') %}
{%- set interface2 = pget(base ~ 'interface2') %}
{%- set address1 = pget(base ~ 'address1') %}
{%- set address2 = pget(base ~ 'address2') %}
{%- set nameserver1 = pget(base ~ 'nameserver1') %}
{%- set nameserver2 = pget(base ~ 'nameserver2') %}
{%- set renderer = pget(base ~ 'renderer') %}
Como mencionei, o arquivo acima tem instruções para obter os dados do Pillar e cria variáveis customizadas que serão usadas no template, esse é um exemplo bem simples e muito bom para podermos começar. Agora vamos entender o que cada linha faz:
{%- set gget = salt['grains.get'] %}
Cria uma variável chamadagget
(Grains Get) usando o módulo grains.get (salt['grains.get']
) para obter dados do Grain. Estamos criando apenas para simplificar, já que o comando é grande.{%- set pget = salt['pillar.get'] %}
Cria uma variável chamadapget
(Pillar Get) usando o módulo pillar.get (salt['pillar.get']
) para obter dados do Pillar. Estamos criando apenas para simplificar, já que o comando é grande.{%- set minionName = gget('host') %}
Cria uma variável chamadaminionName
e essa variável usa a variávelgget
(Grains Get) que tem o módulo grains.get só que abreviado, para obter os nomes dos Minions, ele obtém os nomes dos Minions passando o atributohost
.
Uma forma de testar isso é com o comando abaixo:$ salt '*' grains.get 'host'
zion:
zion
dedsec:
dedsec{%- set base = host ~ ':' %}
Cria uma variável chamadabase
que recebe o nome do Minion concatenado com:
, ou seja, o nome do Minion ficaria assimzion:
. Isso é necessário para quando formos usar o Pillar, já que teremos que informar o nome do Minion com:
mais a chave que vamos obter o valor.{%- set interface1 = pget(base ~ 'interface1') %}
Cria uma variável chamadainterface1
e essa variável usa a variávelpget
(Pillar Get) que tem o módulo pillar.get só que abreviado, para obter o valor que está cadastrado eminterface1
no Pillar, ele obtém esse valor passando o atributointerface1
(Graças apget(base ~ 'interface1')
).{%- set interface2 = pget(base ~ 'interface2') %}
Cria uma variável chamadainterface2
e essa variável usa a variávelpget
(Pillar Get) que tem o módulo pillar.get só que abreviado, para obter o valor que está cadastrado eminterface2
no Pillar, ele obtém esse valor passando o atributointerface2
(Graças apget(base ~ 'interface2')
).{%- set address1 = pget(base ~ 'address1') %}
Cria uma variável chamadaaddress1
e essa variável usa a variávelpget
(Pillar Get) que tem o módulo pillar.get só que abreviado, para obter o valor que está cadastrado emaddress1
no Pillar, ele obtém esse valor passando o atributoaddress1
(Graças apget(base ~ 'address1')
).{%- set address2 = pget(base ~ 'address2') %}
Cria uma variável chamadaaddress2
e essa variável usa a variávelpget
(Pillar Get) que tem o módulo pillar.get só que abreviado, para obter o valor que está cadastrado emaddress2
no Pillar, ele obtém esse valor passando o atributoaddress2
(Graças apget(base ~ 'address2')
).{%- set nameserver1 = pget(base ~ 'nameserver1') %}
Cria uma variável chamadanameserver1
e essa variável usa a variávelpget
(Pillar Get) que tem o módulo pillar.get só que abreviado, para obter o valor que está cadastrado emnameserver1
no Pillar, ele obtém esse valor passando o atributonameserver1
(Graças apget(base ~ 'nameserver1')
).{%- set nameserver2 = pget(base ~ 'nameserver2') %}
Cria uma variável chamadanameserver2
e essa variável usa a variávelpget
(Pillar Get) que tem o módulo pillar.get só que abreviado, para obter o valor que está cadastrado emnameserver2
no Pillar, ele obtém esse valor passando o atributonameserver2
(Graças apget(base ~ 'nameserver2')
).{%- set renderer = pget(base ~ 'renderer') %}
Cria uma variável chamadarenderer
e essa variável usa a variávelpget
(Pillar Get) que tem o módulo pillar.get só que abreviado, para obter o valor que está cadastrado emrenderer
no Pillar, ele obtém esse valor passando o atributorenderer
(Graças apget(base ~ 'renderer')
).
Para testar esse Pillar Get podemos fazer assim:
$ salt 'zion' pillar.get 'zion:address1'
zion:
192.168.121.55/24
$ salt 'dedsec' pillar.get 'dedsec:renderer'
dedsec:
networkd
Agora vamos criar o arquivo de template, ele conterá a configuração de rede para os Minions.
{% import 'network/pillar_network.sls' as pillar_network with context %}
network:
version: 2
renderer: {{ pillar_network.renderer }}
ethernets:
{{ pillar_network.interface1 }}:
dhcp4: false
dhcp6: false
accept-ra: false
addresses:
- {{ pillar_network.address1 }}
gateway4: 192.168.121.1
nameservers:
addresses:
- {{ pillar_network.nameserver1 }}
- {{ pillar_network.nameserver2 }}
{{ pillar_network.interface2 }}:
dhcp4: false
dhcp6: false
accept-ra: false
addresses:
- {{ pillar_network.address2 }}
Agora vamos entender a configuração acima. O corpo do SLS é basicamente a configuração de rede que deve ser realizada, com exceção das variáveis que estamos usando, onde cada variável foi criada no Pillar e definida no arquivo pillar_network.sls
. Já a primeira linha informa o Salt para importar as configurações de network/pillar_network.sls
(Precisa colocar o nome do diretório network porque o ambiente do Salt começa em /srv/salt
), para não ficar colocando network/pillar_network.sls
nós definimos um apelido que é pillar_network
e também aplicamos o contexto.
Agora podemos testar nosso template:
# Primeiro faça um Teste:
$ sudo salt '*' state.apply Test=True
dedsec:
----------
ID: /etc/netplan/
Function: file.directory
Result: None
Comment: The following files will be changed:
/etc/netplan/01-netcfg.yaml: removed - Removed due to clean
/etc/netplan/50-vagrant.yaml: removed - Removed due to clean
/etc/netplan/00-installer-config.yaml: removed - Removed due to clean
Started: 13:32:20.611028
Duration: 8.398 ms
Changes:
----------
/etc/netplan/00-installer-config.yaml:
----------
removed:
Removed due to clean
/etc/netplan/01-netcfg.yaml:
----------
removed:
Removed due to clean
/etc/netplan/50-vagrant.yaml:
----------
removed:
Removed due to clean
----------
ID: /etc/netplan/01-netcfg.yaml
Function: file.managed
Result: None
Comment: The file /etc/netplan/01-netcfg.yaml is set to be changed
Note: No changes made, actual changes may
be different due to other states.
Started: 13:32:20.619491
Duration: 40.129 ms
Changes:
----------
diff:
---
+++
@@ -0,0 +1,22 @@
+
+network:
+ version: 2
+ renderer: networkd
+ ethernets:
+ eth0:
+ dhcp4: false
+ dhcp6: false
+ accept-ra: false
+ addresses:
+ - 192.168.121.161/24
+ gateway4: 192.168.121.1
+ nameservers:
+ addresses:
+ - 8.8.8.8
+ - 8.8.4.4
+ eth1:
+ dhcp4: false
+ dhcp6: false
+ accept-ra: false
+ addresses:
+ - 192.168.100.4/24
Summary for dedsec
------------
Succeeded: 2 (unchanged=2, changed=2)
Failed: 0
------------
Total states run: 2
Total run time: 48.527 ms
zion:
----------
ID: /etc/netplan/
Function: file.directory
Result: None
Comment: The following files will be changed:
/etc/netplan/01-netcfg.yaml: removed - Removed due to clean
/etc/netplan/50-vagrant.yaml: removed - Removed due to clean
/etc/netplan/00-installer-config.yaml: removed - Removed due to clean
Started: 13:32:20.627920
Duration: 8.289 ms
Changes:
----------
/etc/netplan/00-installer-config.yaml:
----------
removed:
Removed due to clean
/etc/netplan/01-netcfg.yaml:
----------
removed:
Removed due to clean
/etc/netplan/50-vagrant.yaml:
----------
removed:
Removed due to clean
----------
ID: /etc/netplan/01-netcfg.yaml
Function: file.managed
Result: None
Comment: The file /etc/netplan/01-netcfg.yaml is set to be changed
Note: No changes made, actual changes may
be different due to other states.
Started: 13:32:20.636269
Duration: 39.756 ms
Changes:
----------
diff:
---
+++
@@ -0,0 +1,22 @@
+
+network:
+ version: 2
+ renderer: networkd
+ ethernets:
+ eth0:
+ dhcp4: false
+ dhcp6: false
+ accept-ra: false
+ addresses:
+ - 192.168.121.55/24
+ gateway4: 192.168.121.1
+ nameservers:
+ addresses:
+ - 9.9.9.9
+ - 8.8.8.8
+ eth1:
+ dhcp4: false
+ dhcp6: false
+ accept-ra: false
+ addresses:
+ - 192.168.100.3/24
Summary for zion
------------
Succeeded: 2 (unchanged=2, changed=2)
Failed: 0
------------
Total states run: 2
Total run time: 48.045 ms
Aparentemente vai dar tudo certo, então vamos aplicar de fato a configuração.
$ sudo salt '*' state.apply
zion:
----------
ID: /etc/netplan/
Function: file.directory
Result: True
Comment: Files cleaned from directory /etc/netplan
Started: 13:32:39.600015
Duration: 7.985 ms
Changes:
----------
/etc/netplan/00-installer-config.yaml:
----------
removed:
Removed due to clean
/etc/netplan/01-netcfg.yaml:
----------
removed:
Removed due to clean
/etc/netplan/50-vagrant.yaml:
----------
removed:
Removed due to clean
removed:
- /etc/netplan/00-installer-config.yaml
- /etc/netplan/01-netcfg.yaml
- /etc/netplan/50-vagrant.yaml
----------
ID: /etc/netplan/01-netcfg.yaml
Function: file.managed
Result: True
Comment: File /etc/netplan/01-netcfg.yaml updated
Started: 13:32:39.608069
Duration: 31.234 ms
Changes:
----------
diff:
New file
mode:
0644
Summary for zion
------------
Succeeded: 2 (changed=2)
Failed: 0
------------
Total states run: 2
Total run time: 39.219 ms
dedsec:
----------
ID: /etc/netplan/
Function: file.directory
Result: True
Comment: Files cleaned from directory /etc/netplan
Started: 13:32:39.611036
Duration: 8.122 ms
Changes:
----------
/etc/netplan/00-installer-config.yaml:
----------
removed:
Removed due to clean
/etc/netplan/01-netcfg.yaml:
----------
removed:
Removed due to clean
/etc/netplan/50-vagrant.yaml:
----------
removed:
Removed due to clean
removed:
- /etc/netplan/50-vagrant.yaml
- /etc/netplan/00-installer-config.yaml
- /etc/netplan/01-netcfg.yaml
----------
ID: /etc/netplan/01-netcfg.yaml
Function: file.managed
Result: True
Comment: File /etc/netplan/01-netcfg.yaml updated
Started: 13:32:39.619230
Duration: 35.302 ms
Changes:
----------
diff:
New file
mode:
0644
Summary for dedsec
------------
Succeeded: 2 (changed=2)
Failed: 0
------------
Total states run: 2
Total run time: 43.424 ms
Agora vamos ver como ficou a configuração de fato em cada um dos Minions.
$ sudo salt '*' cmd.run 'cat /etc/netplan/01-netcfg.yaml'
zion:
network:
version: 2
renderer: networkd
ethernets:
eth0:
dhcp4: false
dhcp6: false
accept-ra: false
addresses:
- 192.168.121.55/24
gateway4: 192.168.121.1
nameservers:
addresses:
- 9.9.9.9
- 8.8.8.8
eth1:
dhcp4: false
dhcp6: false
accept-ra: false
addresses:
- 192.168.100.3/24
dedsec:
network:
version: 2
renderer: networkd
ethernets:
eth0:
dhcp4: false
dhcp6: false
accept-ra: false
addresses:
- 192.168.121.161/24
gateway4: 192.168.121.1
nameservers:
addresses:
- 8.8.8.8
- 8.8.4.4
eth1:
dhcp4: false
dhcp6: false
accept-ra: false
addresses:
- 192.168.100.4/24
Veja que cada Minion ficou com a configuração que colocamos no Pillar. Uma melhoria que poderíamos fazer usar watch
no init.sls
para reiniciar o serviço de rede ou aplicar um comando no netplan que aplique a nova configuração, atualmente ela está apenas no arquivo, mas não entrou em vigor, vou deixar essa tarefa para você caro leitor.
Templates avançados
Agora, vamos criar um template avançado, mais complexo do que os exemplos anteriores. Esse tipo de configuração é ideal para ambientes com muitos servidores, onde é necessário aplicar configurações de forma massiva, mas ainda mantendo flexibilidade para ajustes individuais. Embora o foco seja em uma solução mais robusta, continuaremos aproveitando os conceitos básicos que já abordamos.
Para isso vamos usar o TOFS (Template Override and Files Switch) pattern. Ele é um padrão usado em fórmulas do SaltStack para gerenciar templates e arquivos de configuração de forma flexível e reutilizável. A ideia principal do TOFS é permitir que múltiplos templates coexistam e sejam aplicados de forma seletiva, com base em características específicas dos servidores, como os grains.
O Template Override permite sobrescrever os templates padrão fornecidos por uma fórmula. Se as customizações feitas apenas com dados do Pillar não forem suficientes, é possível criar novos templates específicos para determinadas necessidades ou ambientes. Esses novos templates serão usados no lugar dos padrões originais.
O Files Switch organiza os templates em diretórios que correspondem a diferentes condições dos Minions, como sistema operacional ou outros grains. Quando um estado é aplicado, o Salt seleciona automaticamente o template mais apropriado baseado nos grains do Minion. Assim, em vez de duplicar lógica, você define múltiplas versões de templates e deixa que o Salt faça o trabalho de selecionar o mais adequado.
Primeiro vamos configurar o top.sls
, nesse nosso exemplo vamos criar uma configuração simples apenas para que você leitor consiga entender. No nosso exemplo, vamos criar um template que configure o SSH para exibir uma mensagem durante o login.
base:
'*':
- ssh
Nesse top.sls
estamos informando que se o ID do Minion for zion
, ele deve executar o SLS chamado postfix
. Agora crie os diretórios como de costume.
# Crie o diretório onde ficará o SLS:
sudo mkdir /srv/salt/ssh
# Crie um diretório dentro dele para armazenar apenas os templates que serão
# enviados para os minions:
sudo mkdir /srv/salt/ssh/files
Agora vamos configurar o top do Pillar, novamente.
base:
'G@id:zion':
- zion
'G@id:dedsec':
- dedsec
Agora vamos configurar o pillar/zion.sls
e depois o pillar/dedsex.sls
. Esses arquivos terão as variáveis de configuração para configurar o servidor, cada servidor terá variáveis exclusivamente de cada um. Vou criar variáveis de configurações para o Postfix e Dovecot, dessa forma você poderá ter uma boa base de como se faz.
ssh:
banner:
- name: issue.net
serverName: zion.com.br
companyName: zion
firewall:
ipv4:
- name: rules.v4
file: /etc/iptables/rules.v4
ssh:
banner:
- name: issue.net
serverName: dedsec.com.br
companyName: dedsec
Agora vamos configurar o SLS do nosso template.
{%- set config_pillar = salt['config.get'](tpldir, default={}) %}
ssh_banner:
file.managed:
- name: /etc/issue.net
- source: salt://ssh/files/issue.net
- template: jinja
- context: {{ config_pillar }}
- user: root
- group: root
- mode: 640
set_ssh_banner:
file.append:
- name: /etc/ssh/sshd_config
- text: 'Banner /etc/issue.net'
service.running:
- name: sshd
- reload: True
- enable: True
- watch:
- file: /etc/ssh/sshd_config
Agora vamos configurar o arquivo de template:
{% if banner is defined %}
{% for var in banner %}
*********************************************************************************
ATENÇÃO: ACESSO RESTRITO AO SERVIDOR {{ var.serverName }}
Este sistema é de uso exclusivo de funcionários autorizados da Empresa {{ var.companyName }}.
Qualquer acesso ou tentativa de acesso não autorizado será monitorado e pode
resultar em ações disciplinares e/ou legais. Se você não está autorizado,
desconecte imediatamente.
Todos os acessos são registrados.
*********************************************************************************
{% endfor %}
{% endif %}
Agora é só executar o Salt e ver a mágica acontecer.
$ sudo salt '*' state.apply
zion:
----------
ID: ssh_banner
Function: file.managed
Name: /etc/issue.net
Result: True
Comment: File /etc/issue.net updated
Started: 16:56:21.559595
Duration: 17.591 ms
Changes:
----------
diff:
New file
mode:
0640
----------
ID: set_ssh_banner
Function: file.append
Name: /etc/ssh/sshd_config
Result: True
Comment: Appended 1 lines
Started: 16:56:21.577267
Duration: 2.195 ms
Changes:
----------
diff:
---
+++
@@ -121,3 +121,4 @@
# PermitTTY no
# ForceCommand cvs server
PermitRootLogin yes
+Banner /etc/issue.net
----------
ID: set_ssh_banner
Function: service.running
Name: sshd
Result: True
Comment: Service reloaded
Started: 16:56:21.624874
Duration: 636.675 ms
Changes:
----------
sshd:
True
Summary for zion
------------
Succeeded: 3 (changed=3)
Failed: 0
------------
Total states run: 3
Total run time: 656.461 ms
dedsec:
----------
ID: ssh_banner
Function: file.managed
Name: /etc/issue.net
Result: True
Comment: File /etc/issue.net updated
Started: 16:56:21.572149
Duration: 17.422 ms
Changes:
----------
diff:
New file
mode:
0640
----------
ID: set_ssh_banner
Function: file.append
Name: /etc/ssh/sshd_config
Result: True
Comment: Appended 1 lines
Started: 16:56:21.589647
Duration: 2.1 ms
Changes:
----------
diff:
---
+++
@@ -121,3 +121,4 @@
# PermitTTY no
# ForceCommand cvs server
PermitRootLogin yes
+Banner /etc/issue.net
----------
ID: set_ssh_banner
Function: service.running
Name: sshd
Result: True
Comment: Service reloaded
Started: 16:56:22.310488
Duration: 43.1 ms
Changes:
----------
sshd:
True
Summary for dedsec
------------
Succeeded: 3 (changed=3)
Failed: 0
------------
Total states run: 3
Total run time: 62.622 ms
Veja os arquivos em cada servidor:
$ sudo salt '*' cmd.run 'cat /etc/issue.net'
zion:
*********************************************************************************
ATENÇÃO: ACESSO RESTRITO AO SERVIDOR zion.com.br
Este sistema é de uso exclusivo de funcionários autorizados da Empresa zion.
Qualquer acesso ou tentativa de acesso não autorizado será monitorado e pode
resultar em ações disciplinares e/ou legais. Se você não está autorizado,
desconecte imediatamente.
Todos os acessos são registrados.
*********************************************************************************
dedsec:
*********************************************************************************
ATENÇÃO: ACESSO RESTRITO AO SERVIDOR dedsec.com.br
Este sistema é de uso exclusivo de funcionários autorizados da Empresa dedsec.
Qualquer acesso ou tentativa de acesso não autorizado será monitorado e pode
resultar em ações disciplinares e/ou legais. Se você não está autorizado,
desconecte imediatamente.
Todos os acessos são registrados.
*********************************************************************************
Agora ao tentar fazer login via SSH podemos ver o Banner:
$ ssh vagrant@192.168.121.55
*********************************************************************************
ATENÇÃO: ACESSO RESTRITO AO SERVIDOR zion.com.br
Este sistema é de uso exclusivo de funcionários autorizados da Empresa zion.
Qualquer acesso ou tentativa de acesso não autorizado será monitorado e pode
resultar em ações disciplinares e/ou legais. Se você não está autorizado,
desconecte imediatamente.
Todos os acessos são registrados.
*********************************************************************************
vagrant@192.168.121.55's password:
Explicando as configuração
Vamos revisar a estrutura que foi criada para configurar o banner do SSH, utilizando o SaltStack e o Pillar, e entender como tudo funciona.
$ sudo tree /srv/salt/
/srv/salt/
├── pillar
│ ├── dedsec.sls
│ ├── top.sls
│ └── zion.sls
├── ssh
│ ├── files
│ │ └── issue.net
│ └── init.sls
└── top.sls
3 directories, 6 files
O arquivo top.sls
no diretório principal do Salt está configurado para aplicar o estado ssh
, que está definido dentro do diretório /srv/salt/ssh
. Já o arquivo top.sls
do Pillar está configurado para fornecer variáveis de configuração diferentes para cada minion, com base no seu ID. Isso permite que cada minion receba configurações específicas, como o banner SSH personalizado.
Com relação ao Pillar você deve ter notado que eu configurei algumas opções a mais no Pillar do Zion, eu fiz isso para demonstrar como algumas coisas funcionam, já volto para explicar o motivo. Sobre o init.sls
, ele é o arquivo que será executado por padrão sempre que o SLS ssh
for executado. Dentro do init.sls
, temos a seguinte linha:
{%- set config_pillar = salt['config.get'](tpldir, default={}) %}
Essa linha cria uma variável chamada config_pillar
, que recebe o valor retornado pelo comando salt['config.get'](tpldir, default={})
. Esse comando busca dados específicos do Pillar para o minion que está sendo executado. A variável tpldir
contém o nome do diretório do SLS (ssh
), o que significa que estamos buscando as variáveis que começam com ssh:
no Pillar.
O valor de tpldir
reflete o diretório onde o SLS está localizado. No caso, como o arquivo init.sls
está em /srv/salt/ssh/
, o valor de tpldir
será ssh
, e os dados buscados no Pillar serão aqueles definidos sob a chave ssh:
. Podemos testar esse comportamento da seguinte forma:
# Antes de obter os dados do Pillar, sinalize os minions para atualizar os dados do pilar na memória.
$ sudo salt '*' saltutil.pillar_refresh
dedsec:
True
zion:
True
# Agora obtenha os dados do 'ssh' que estão no Pillar:
$ sudo salt 'zion' config.get ssh
zion:
----------
banner:
|_
----------
companyName:
zion
name:
issue.net
serverName:
zion.com.br
Como podemos ver, o SaltStack retorna as informações do Pillar que estão sob a chave ssh
, e esses dados serão usados para configurar o banner nos servidores. Se tivéssemos um diretório chamado firewall
, o comando seria o seguinte:
# Antes de obter os dados do Pillar, sinalize o minion para atualizar os dados do pilar na memória.
$ sudo salt '*' saltutil.pillar_refresh
dedsec:
True
zion:
True
# Agora obtenha os dados do 'ssh' que estão no Pillar:
$ sudo salt 'zion' config.get firewall
zion:
----------
ipv4:
|_
----------
file:
/etc/iptables/rules.v4
name:
rules.v4
Na próxima parte do init.sls
, estamos dizendo ao Salt para enviar um arquivo do Master para o Minion, e estamos usando o template Jinja para renderizar o conteúdo dinamicamente. A nova opção aqui é o - context: {{ config_pillar }}
. Essa opção é essencial para passar dados dinâmicos (do Pillar ou de outras fontes) para o template Jinja que será renderizado. Sem o context
, o SaltStack não saberia quais variáveis devem ser usadas no processo de renderização, e o template não funcionaria corretamente.
O argumento context
permite que você injete variáveis como um dicionário dentro do template. No nosso caso, o config_pillar
contém as informações extraídas de salt['config.get'](tpldir, default={})
, que busca as configurações do Pillar baseadas no nome do diretório do SLS. Exemplo da parte mencionada:
ssh_banner:
file.managed:
- name: /etc/issue.net
- source: salt://ssh/files/issue.net
- template: jinja
- context: {{ config_pillar }}
- user: root
- group: root
- mode: 640
Aqui, o SaltStack usa as informações armazenadas em
config_pillar
para preencher o templateissue.net
e, em seguida, coloca o arquivo no caminho correto no servidor.
A última parte do init.sls
garante que, se o arquivo /etc/ssh/sshd_config
for modificado (quando o banner é adicionado), o serviço SSH será recarregado automaticamente, isso é necessário para fazer o banner entrar em Vigor. Isso é feito com o comando watch
.
set_ssh_banner:
file.append:
- name: /etc/ssh/sshd_config
- text: 'Banner /etc/issue.net'
service.running:
- name: sshd
- reload: True
- enable: True
- watch:
- file: /etc/ssh/sshd_config
O arquivo de template em ssh/files/issue.net
começa com uma condição Jinja:
{% if banner is defined %}
{% for var in banner %}
Essa condição verifica se a variável banner
está definida (no Pillar). O renderizador Jinja sabe que banner
existe porque ela foi passada através do - context
. Dentro do loop, a variável var
representa cada entrada do dicionário banner
, e você pode acessar suas chaves (como serverName
e companyName
) diretamente, como mostrado aqui:
ATENÇÃO: ACESSO RESTRITO AO SERVIDOR {{ var.serverName }}
Este sistema é de uso exclusivo de funcionários autorizados da Empresa {{ var.companyName }}.
Isso permite que o banner seja personalizado para cada servidor, com base nos dados do Pillar.
Renderizadores
Até o momento, vimos o uso do import
tradicional no SaltStack, que é utilizado para importar dados de arquivos SLS. Agora, vamos aprender como importar dados de um arquivo YAML, o que pode ser útil em cenários onde precisamos de uma fonte externa de dados para configurar o estado de múltiplos minions. Vamos criar um exemplo teórico para mostrar como isso funciona na prática. No exemplo abaixo, vamos importar dados de um arquivo YAML para o SLS usando o import_yaml
:
{%- import_yaml tpldir ~ "/base_info.yaml" as default_set %}
vars:
cmd.run:
- name: "echo 'O conteúdo de base_info.yaml é: {{ default_set }}'"
Aqui, estamos usando a diretiva import_yaml
para importar o conteúdo de um arquivo YAML chamado base_info.yaml
e armazená-lo na variável default_set
. A variável tpldir
refere-se ao diretório onde o SLS atual está localizado, o que nos permite montar o caminho para o arquivo a ser importado.
Agora, vamos criar o arquivo base_info.yaml
, que conterá os dados que queremos usar em nosso estado:
pkg:
pkg_name: vim dnsutils zsh
ssh_banner: /etc/issue.net
Ao executar o SLS, veremos o conteúdo do arquivo YAML sendo impresso:
$ sudo salt zion state.apply pkg | grep -A1 'stdout:'
stdout:
O conteúdo de base_info.yaml é: {pkg: {pkg_name: vim dnsutils zsh, ssh_banner: /etc/issue.net}}
Apesar de ser uma abordagem funcional, você deve ter cuidado ao usar arquivos YAML como este para armazenar dados de configuração. Diferente do Pillar, que fornece dados específicos para cada minion, os dados armazenados em um arquivo YAML como este estarão acessíveis para todos os minions. Isso pode ser útil quando os dados são globais e comuns a todos os servidores, mas para configurações mais sensíveis ou específicas de minion, o Pillar ainda é a melhor solução.
Agora que importamos os dados do arquivo YAML, podemos usá-los como qualquer outra variável no SaltStack. Por exemplo, podemos acessar os pacotes armazenados na chave pkg_name
com {{ default_set.pkg.pkg_name }}
. Porém, observe que o valor pkg_name
é uma string que contém vários pacotes separados por espaços. Para instalar esses pacotes de forma eficiente, precisamos convertê-los em uma lista, para que possa ser usado no for
do Salt. Para isso, podemos usar o seguinte comando:
{%- set pkg_list = default_set.pkg.pkg_name.split() %}
Esse comando divide a string em uma lista, permitindo que possamos iterar sobre ela e instalar os pacotes individualmente.
Agora que temos os pacotes convertidos em uma lista, podemos criar um serviço para instalá-los de forma dinâmica:
{%- import_yaml tpldir ~ "/base_info.yaml" as default_set %}
{%- set pkg_list = default_set.pkg.pkg_name.split() %}
{% for package in pkg_list %}
{{ package }}:
pkg.installed:
- name: {{ package }}
{% endfor %}
Carregando Render e Pillar na Memória
Em ambientes complexos, onde múltiplos servidores possuem diferentes configurações, é essencial garantir que os dados de diferentes fontes, como arquivos YAML e o Pillar, estejam carregados na memória e disponíveis para uso durante a execução dos estados do SaltStack. Normalmente, utiliza-se um arquivo chamado map.jinja
, mas você pode escolher o nome que preferir. Esse arquivo é amplamente utilizado em fórmulas do SaltStack para armazenar variáveis dinâmicas, como listas de pacotes, caminhos de arquivos e outras configurações específicas de sistemas, permitindo que o SaltStack aplique essas configurações de forma inteligente e personalizada com base nos grains (como sistema operacional, versão ou outras características do minion).
{%- import_yaml tpldir ~ "/base_info.yaml" as base_settings %}
{%- set pillar_settings = salt['config.get'](tpldir, default={}) %}
{%- set defaults = salt['grains.filter_by'](
base_settings,
default=tpldir,
merge=salt['grains.filter_by'](
pillar_settings
)
)
%}
{%- set config = salt['grains.filter_by'](
{'defaults': defaults},
default='defaults',
merge=pillar_settings
)
%}
{%- set settings = config %}
O import_yaml
é usado para importar um arquivo YAML (base_info.yaml
) e o armazena na variável base_settings
. O tpldir
representa o diretório base do SLS atual, o que significa que o caminho do arquivo base_info.yaml
está dentro do SLS que está sendo executado. Essa linha é usada para carregar os dados do arquivo base_info.yaml
.
O comando salt['config.get'](tpldir, default={})
busca no Pillar os dados que começam com a chave representada por tpldir
(é o nome do diretório SLS que está sendo executado). Os dados que foram obtidos do Pillar são armazenados na variável pillar_settings
. Caso não existam dados no Pillar que correspondam com o nome do diretório SLS que está sendo usado, um dicionário vazio ({}
) é atribuído por padrão.Essa linha é usada para carregar os dados específicas do servidor a partir do Pillar, usadas para personalizar a configuração do Minion.
Para que os dados consigam ser obtidos corretamente do pillar, você deve ter uma chave no Pillar exatamente com o nome do diretório SLS. Exemplo, o diretório SLS se chama postfix
, então para obter corretamente os dados no pillar, temos que ter a chave postfix:
no Pillar, exemplo:
postfix:
conf:
main:
O grains.filter_by
é usado para filtrar dados de acordo com os grains do Minion (informações específicas do sistema, como os_family
, os
, etc.). O default_settings
é o primeiro argumento, ele contém os dados importados de base_info.yaml
, usaremos esses dados para criação de um filtro. O default=tpldir
define que, a chave lookup_dict é usada se o grain não existir ou se o valor do grain não tiver correspondência em lookup_dict. Se não for especificado, o valor é default. Basicamente é usado para informar qual a chave inicial deve ser exibida.
Normalmente, você define o parâmetro default
para determinar o que fazer se o grain do minion não corresponder a nenhuma das chaves no dicionário de busca (lookup_dict
). O valor padrão normalmente é "default"
.
O merge=salt['grains.filter_by'](pillar_settings, default='lookup')
faz uma mesclagem dos dados do Pillar (pillar_settings
) com os dados do arquivo YAML (default_settings
). O default='lookup'
define qual chave do dicionário fornecido a grains.filter_by
deve ser usada caso o valor do grain não corresponda a nenhuma das chaves no dicionário. Essa etapa combina dados padrão (do YAML) com dados mais específicos (do Pillar), aplicando-os de forma condicional de acordo com os grains do minion.
O segundo grains.filter_by
usa como primeiro argumento um dicionário ({ 'defaults': defaults }
), aqui estamos dizendo que a chave defaults
recebe todo os daos do primeiro grains.filter_by
. O default='defaults'
define que, se não houver um grain específico, ele deve usar o valor padrão em 'defaults'
, ou seja, os dados obtidos acima. O merge=pillar_settings
mescla os dados do Pillar novamente. No final toda essa informação é colocada na variável config.
Isso resulta na configuração final que será aplicada ao Minion, combinando dados do arquivo YAML, do Pillar e aplicando lógica condicional com base nos grains.
Por fim declaramos uma variável chamada settings
que recebe todo o valor de config
. Quando formos obter os valores, vamos sempre colocar settings
primeiro, para referenciar que queremos esse valor.
Agora crie um SLS que vá usar essa informação, similar ao /srv/salt/ssh/init.sls
, a única diferença será a primeira linha e o context
:
{%- from tpldir ~ "/map.jinja" import settings with context %}
ssh_banner:
file.managed:
- name: /etc/issue.net
- source: salt://ssh/files/issue.net
- template: jinja
- context: {{ settings }}
- user: root
- group: root
- mode: 640
set_ssh_banner:
file.append:
- name: /etc/ssh/sshd_config
- text: 'Banner /etc/issue.net'
service.running:
- name: sshd
- reload: True
- enable: True
- watch:
- file: /etc/ssh/sshd_config
Após isso para referenciar as variáveis é igual foi apresentado.
Como testar?
Uma forma de testar tudo é da seguinte maneira:
{%- import_yaml tpldir ~ "/base_info.yaml" as base_settings %}
{%- set pillar_settings = salt['config.get'](tpldir, default={}) %}
tpldir_value:
cmd.run:
- name: "echo 'tpldir é: {{ tpldir }}'"
base_settings_value:
cmd.run:
- name: "echo 'base_settings é: {{ base_settings }}'"
Agora é só executar o SLS:
$ sudo salt zion state.apply pkg | grep -A1 'stdout:'
stdout:
tpldir é: pkg
--
stdout:
base_settings é: {pkg: {pkg_name: vim dnsutils zsh, ssh_banner: /etc/issue.net}}
Com os dados so SLS, nós podemos testar via ad-hoc. Vamos começar testando a obtenção dos dados que estão do Pillar.
# Pegue o dicionário de 'base_settings' e coloque no comando abaixo:
$ sudo salt 'zion' grains.filter_by '{pkg: {pkg_name: vim dnsutils zsh, ssh_banner: /etc/issue.net}}' default='pkg'
zion:
----------
pkg_name:
vim dnsutils zsh
ssh_banner:
/etc/issue.net
Dessa forma podemos começar a entender como é construído do map.jinja. Você pode ver os valores das variáveis (principalmente do settings
) com o init.sls
configurado da seguinte forma:
{%- from tpldir ~ "/map.jinja" import settings with context %}
settings_value:
cmd.run:
- name: "echo 'settings é: {{ settings }}'"
ssh_banner_value:
cmd.run:
- name: "echo 'ssh_banner é: {{ settings.ssh_banner }}'"
install_value:
cmd.run:
- name: "echo 'Os pacotes para serem instalados são: {{ settings.install }}'"
Agora é só executar o comando abaixo:
$ sudo salt zion state.apply pkg | grep -A1 'stdout:'
stdout:
settings é: {pkg_name: vim dnsutils zsh, ssh_banner: /etc/issue.net, install: [{name: vim zsh}], remove: [{name: bash snapd}]}
--
stdout:
ssh_banner é: /etc/issue.net
--
stdout:
Os pacotes para serem instalados são: [{name: vim zsh}]
Simplificando os exemplos
Nós vimos como mesclar os dados de um arquivo yaml
e do Pillar
na memória, para que ambos ficassem disponível num único lugar, o processo é complexo e difícil. Vamos ver como simplificar isso.
{%- import_yaml tpldir ~ "/base_info.yaml" as base_settings %}
{%- set pillar_settings = salt['config.get'](tpldir, default={}) %}
{% do base_settings.ssh.update(pillar_settings) %}
{% set new_settings = salt['pillar.get']('ssh', default=base_settings.ssh, merge=True) %}
A linha {% do base_settings.pkg.update(pillar_settings) %}
está mesclando os dados de pillar_settings
(obtidos do Pillar) com o base_settings.ssh
(dados do arquivo YAML), sobrescrevendo ou adicionando valores de pillar_settings
ao base_settings.ssh
. Resumindo, estamos mesclando pillar_settings
no dicionário base_settings.ssh
(é obrigatório informar uma chave dentro do dicionário para saber onde colocar os dados, nesse caso a chave é a ssh
, porque estamos no SLS ssh).
A linha {% set new_settings = salt['pillar.get']('ssh', default=base_settings.ssh, merge=True) %}
busca os dados da chave ssh
no Pillar. Se não houver dados, usa base_settings.ssh
como valor padrão. O parâmetro merge=True
mescla os dados do Pillar com os valores de base_settings.ssh
, priorizando os dados do Pillar.
Fontes
https://docs.saltproject.io/en/latest/ref/configuration/nonroot.html#configuration-non-root-user
https://docs.saltproject.io/en/latest/ref/runners/all/salt.runners.jobs.html
https://docs.saltproject.io/en/latest/ref/publisheracl.html
https://docs.saltproject.io/en/latest/topics/eauth/access_control.html
https://docs.saltproject.io/en/latest/topics/eauth/index.html
https://docs.saltproject.io/salt/user-guide/en/latest/topics/security.html