[Из песочницы] SaltStack: использование salt-ssh

salt-sshВ этом посте я хотел бы поделиться своим опытом использования системы управления конфигурациями SaltStack, и, в частности, её применением в Masterless режиме при помощи salt-ssh компонента.

По сути, salt-ssh является аналогом системы Ansible.

salt-ssh '*-ec2.mydomain.com' test.ping

Будут затронуты следующие темы:


  • Почему SaltStack, ключевые особенности
  • Базовые понятия SaltStack
  • Salt-ssh установка и использование


Почему SaltStack, ключевые особенности

Когда я несколько лет назад, вдоволь насытившись puppet (multiple environments, 100+ nodes), для нового проекта выбирал новую же систему управления конфигурациями, то Master less режим работы был ключевым требованием. Но также хотелось сохранить возможность Master-Slave режима работы. Хотелось хорошей, обширной документации и гибкости. Хотелось уметь управлять облачными инфраструктурами.

А еще хотелось построить систему, в которой будут легко уживаться самые разные окружения. Все это удалось сделать при помощи salt-ssh.

Salt-ssh это компонент SaltStack, который, как и Ansible, использует ssh для соединения с удаленными машинами и не требует никаких предварительных настроек со стороны удаленных машин. Никаких агентов. Pure ssh!

Конечно, при выборе системы я рассматривал и Ansible. Но чаша весов тогда склонилась к SaltStack.

В отличие от Ansible, SaltStack использует Jinja2 как для обработки шаблонов, так и для построения логики.

Причем эту логику можно построить практически любыми способом. Что с одной стороны и хорошо и плохо. Хорошо т.к. даёт гибкость. Но плохо, т.к. нет одного стандартного способа и подхода к реализации. Мне даже кажется, что SaltStack больше представляет из себя конструктор в этом плане.

Также, рендеринг шаблонов и логики происходит на этапе запуска. Полученный пакет шаблонов, настроек и инструкций копируется на удаленный сервер и выполняется. По завершению выполнения salt-ssh выдает в консоль отчет, что было выполнено, а с чем произошли ошибки, если они возникли. Здесь отличие с ansible очень разительно. Последний выполняет задачи\плейбуки поочередно, в режиме shell-скрипта. Не скрою, что наблюдать за ходом выполнения ansible скриптов приятнее, но постепенно все это отходит на второй план, когда число хостов переваливает за несколько десятков. Также, в с равнении с ansible, SaltStack имеет более высокий уровень абстракции.
Как бы то ни было, и ansible и salt-ssh это два очень интересных инструмента, каждый из которых имеет свои плюсы и минусы.


Базовые понятия SaltStack

SaltStack — это система управления конфигурациями и инфраструктурой. Как на уровне отдельных серверов, так и в различных облачных платформах (SaltCloud). Также это система удаленного выполнения команд. Написана на python. Очень бурно развивается. Имеет множество самых разных модулей и возможностей, в том числе даже такие как Salt-Api и Salt-Syndic (master of masters или система, позволяющая строить иерархию мастер серверов, тобишь синдикат).

По умолчанию SaltStack подразумевает Master-Slave режим работы. Обмен сообщениями между нодами происходит через ZeroMQ протокол. Умеет горизонтально масштабироваться при помощи MultiMaster настроек.

Но самое приятное, Salt умеет еще работать и в Agent Less режиме. Что Может быть реализовано при помощи локального запуска стейтов либо при помощи salt-ssh, героя данного топика.

Salt master — процесс, работающий на машине, с которой происходит управление подключенными агентами. В случае с salt-ssh можно назвать мастером ту ноду, где лежат наши state и pillar данные
Salt minion — процесс, работающий на управляемых машинах, т.е. slave. В случае с salt-ssh minion это любой удаленный сервер
State — декларативное представление состояния системы (аналог playbooks в ansible)
Grains — статическая информация об удаленном minion (RAM, CPUs, OS, etc)
Pillar — переменые для одного или более minions
top.sls — центральные файлы, реализующие логику какие state и pillar данные какому minion назначить
highstate — все определенные state данные для minion
SLS — так называются все конфигурационные файлы для pillar\states в SaltStack, используется YAML

Одним из недостатков системы SaltStack является более высокий порог вхождения. Далее я покажу примеры, чтобы начать работать с этой замечательной системой было проще.


Salt-ssh установка и использование

Установка salt-ssh происходит тривиально.

На сайте https://repo.saltstack.com/ есть все необходимые репозитории и инструкции для подключения их в разнообразных системах.

Для установки нужен только сам salt-ssh.

sudo apt-get install salt-ssh (На примере Deb систем)


Подготовка тестового окружения и Vagrant

Чтобы начать использовать salt-ssh нам достаточно установить его. Как минимум можно управлять своей локальной машиной, либо любым удаленным сервером, что гораздо наглядней.

В данном же примере, для тестов я буду использовать две виртуальные машины, созданные при помощи Vagrant. На одной из них будет установлен собственно сам salt-ssh, другая будет чистая, не считая подключенного публичного ключа с первой машины.

Сам Vagrantfile и необходимые salt states выложены в репозиторий https://github.com/skandyla/saltssh-intro.


Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure(2) do |config|

  # VM with salt-ssh
  config.vm.define :"saltsshbox" do |config|
    config.vm.box = "ubuntu/trusty64"
    config.vm.hostname = "saltsshbox"
    config.vm.network "private_network", ip: "192.168.33.70"
    config.vm.provider "virtualbox" do |vb|
      vb.memory = "512"
      vb.cpus = 2
    end

    config.vm.synced_folder ".", "/srv"

    # Deploy vagrant insecure private key inside the VM
    config.vm.provision "file", source: "~/.vagrant.d/insecure_private_key", destination: "~/.ssh/id_rsa"

    # Install salt-ssh
    config.vm.provision "shell", inline: <<-SHELL
      wget -O - https://repo.saltstack.com/apt/ubuntu/14.04/amd64/latest/SALTSTACK-GPG-KEY.pub | sudo apt-key add -
      sudo echo 'deb http://repo.saltstack.com/apt/ubuntu/14.04/amd64/latest trusty main' > /etc/apt/sources.list.d/saltstack.list
      sudo apt-get update
      sudo apt-get install -y salt-ssh
    SHELL
  end

  # VM for testing
  config.vm.define :"testserver" do |config|
    config.vm.box = "ubuntu/trusty64"
    config.vm.hostname = "testserver"
    config.vm.network "private_network", ip: "192.168.33.75"
    config.vm.provider "virtualbox" do |vb|
      vb.memory = "512"
    end

    # Deploy vagrant public key
    config.vm.provision "shell", inline: <<-SHELL
      curl https://raw.githubusercontent.com/mitchellh/vagrant/master/keys/vagrant.pub >> ~/.ssh/authorized_keys2 2>/dev/null
      curl https://raw.githubusercontent.com/mitchellh/vagrant/master/keys/vagrant.pub >> /home/vagrant/.ssh/authorized_keys2 2>/dev/null
    SHELL
  end

end

Я предполагаю что аудитория знакома с Vagrant, но на всякий случай: Vagrant — это своееобразный фреймворк для систем виртуализации, предназначенный для упрощения процесса разработки и создания его воспроизводимым. Для запуска виртуальных машин нам понадобятся установленными сам Vagrant и Virtualbox.

Дальше клонируем репозиторий:

git clone https://github.com/skandyla/saltssh-intro

и в нем инициализируем Vagrant виртуальные машины:

vagrant up

После запуска последних, заходим в saltsshbox:

vagrant ssh saltsshbox

Вся дальнейшая работа будет вестись из данной виртуальной машины. По умолчанию SaltStack предполагает что мы будет действовать от имени root, поэтому сразу делаем:

vagrant@saltsshbox:~$ sudo -i


Понимание salt roster

Целевые хосты прописываются в файл /etc/salt/roster, однако, можно указать любой сторонний roster file. В некотором смысле можно провести аналогии с inventory файлами ansible. Roster файл, представляет собой YAML, с множеством разных опций. Ниже показаны несколько способов записи одного и того же хоста.


/srv/saltstack/saltetc/roster_test
testserver:
  host: 192.168.33.75
  priv: /home/vagrant/.ssh/id_rsa

thesametestserver:
  host: 192.168.33.75
  user: vagrant
  sudo: True

thesametestserver2:
  host: 192.168.33.75
  user: vagrant
  passwd: vagrant
  sudo: True

Теперь попробуем выполнить команду test.ping применительно ко всем хостам, указанным в нашем ростере.

root@saltsshbox:~# salt-ssh -i --roster-file=/srv/saltstack/saltetc/roster_test '*' test.ping
Permission denied for host thesametestserver, do you want to deploy the salt-ssh key? (password required):
[Y/n] n
thesametestserver:
    ----------
    retcode:
        255
    stderr:
        Permission denied (publickey,password).
    stdout:
testserver:
    True
thesametestserver2:
    True

Как видите, salt-ssh слегка ругнулся, что не может зайти на удаленный сервер и предложил туда развернуть ключ, но я отменил это. Остальные два сервера (по факту один под разными именами) ответили положительно. Произошло это потому что мы работает из-под root, для которого никаких ssh-ключей не определено. Поэтому можно просто добавить ключ через ssh-agent и снова повторить команду.


повтор:
root@saltsshbox:~# eval `ssh-agent`; ssh-add /home/vagrant/.ssh/id_rsa
Agent pid 2846
Identity added: /home/vagrant/.ssh/id_rsa (/home/vagrant/.ssh/id_rsa)
root@saltsshbox:~# salt-ssh -i --roster-file=/srv/saltstack/saltetc/roster_test '*' test.ping
testserver:
    True
thesametestserver:
    True
thesametestserver2:
    True

Теперь все хорошо! Мало того, можно запросто добавить ключ с паролем через ssh-agent. Но если же вы решите деплоить ключ, который предлагает сам salt, то брать его по умолчанию он будет вот по такому пути: /etc/salt/pki/master/ssh/salt-ssh.rsa

Здесь для теста я намеренно работал с отдельным roster файлом, чтобы показать интересные нюансы. Для дальнейшей же работы указывать roster нам будет не нужно, потому что он уже и так через symlink указан в требуемое место (/etc/salt/roster). Ключ -i необходим когда мы начинаем работать с новыми хостами, он просто запрещает StrictHostKeyChecking, давая возможность принять новый host key. Для дальнейшей работы он нам тоже будет не нужен.

root@saltsshbox:~# salt-ssh '*' test.ping
testserver:
    True

Напомню, что по умолчанию salt смотрит на roster здесь: /etc/salt/roster в котором у нас сейчас определен только один хост.


Удаленное выполнение команд

Теперь когда мы убедились что наша машина с salt-ssh прекрасно видит указанный в roster тестовый сервер, поработаем с ним в стиле ad-hoc.

root@saltsshbox:~# salt-ssh testserver cmd.run "uname -a"
testserver:
    Linux testserver 3.13.0-87-generic #133-Ubuntu SMP Tue May 24 18:32:09 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

cmd.run это по сути аналог ключа -a в ansible.

Также можно использовать встроенные модуля saltstack, например:

salt-ssh testserver service.get_enabled
salt-ssh testserver pkg.install git
salt-ssh testserver network.interfaces
salt-ssh testserver disk.usage
salt-ssh testserver sys.doc 

Последняя команда выдаст документацию об модулях и, главное, примеры их использования. Дополнительно можно посмотреть полный список доступных модулей Saltstack.


Salt Grains или факты о системе

Grains это мощный механизм, представляющий набор фактов об удаленной системе. В дальнейшем на основе Grains можно также строить различную логику.

Но для начала посмотрим как с ними начать работать:

root@saltsshbox:~# salt-ssh testserver grains.items
testserver:
    ----------
    SSDs:
    biosreleasedate:
        12/01/2006
    biosversion:
        VirtualBox
    cpu_flags:
        - fpu
        - vme
        - de
        - pse
        - tsc
...
    cpu_model:
        Intel(R) Core(TM) i7-2620M CPU @ 2.70GHz
    cpuarch:
        x86_64
    disks:
        - sda
...

Вывод команды урезан.

К нужной ветви Grains можно обратиться, непосредственно указав её:

root@saltsshbox:~# salt-ssh testserver grains.get 'ip4_interfaces'
testserver:
    ----------
    eth0:
        - 10.0.2.15
    eth1:
        - 192.168.33.75
    lo:
        - 127.0.0.1

Либо даже более конкретно:

root@saltsshbox:~# salt-ssh testserver grains.get 'ip4_interfaces:eth1'
testserver:
    - 192.168.33.75


Salt master файл и top.sls

Теперь пришла пора рассказать про еще один важный файл /etc/salt/master. Вообще он идет в комплекте с пакетом salt-master и определяет некоторые важные опции логирования и директории в которых salt будет искать наши states и pillar данные. По умолчанию для states определена директория /srv/salt. Но на практике часто более рационально использовать другую структуру, в том числе и для этих примеров.

/etc/salt/master:

state_verbose: False
state_output: mixed
file_roots:
  base:
    - /srv/saltstack/salt
pillar_roots:
  base:
    - /srv/saltstack/pillar

state_verbose и state_output это переменные, которые отвечают за отображение статуса выполнения на экране. На мой взгляд такая комбинация наиболее практична, но рекомендую поэкспериментировать.

file_roots и pillar_roots указывают пути к нашим state и pillar данным соотвественно.

Важно! этих путей может быть несколько. По принципу разных окружений, разных данных и т.д. и т.п., но это тема для отдельной статьи по настройке multi-environment среды, для начала нам же просто необходимо знать, куда нам положить наши state файлы, чтобы salt их нашел.

Далее, в каждой из этой директорий (file_roots и pillar_roots) сальт будет искать файлы top.sls, которые определяют уже дальнейшую логику обработки salt файлов.

В нашем случае:

/srv/saltstack/salt/top.sls:

base:                                                                                
  '*':                                                                               
    - common
    - timezone

  'testserver':
    - chrony

Что означает, для всех хостов применить state common и timezone, а для testserver применить еще и chrony (сервис синхронизации времени).

Для pillar также необходим top.sls файл. Который будет определять в каком порядке и как будут назначаться переменные.
/srv/saltstack/pillar/top.sls:

base:
  '*':
    - timezone

  'testserver':
    - hosts/testserver

В нашем случае файл этот предельно простой, указано лишь подключить все переменные из файла timezone.sls и еще подключить переменые из файла hosts/testserver для нашего testserver, однако, за этой простотой скрывается мощная концепция, т.к. переменные можно назначить как угодно и на какое угодно окружение. Правда, перекрытие и слияние переменных (Variables Overriding and Merging) это отдельная тема, пока скажу что приоритетность задается сверху вниз. Т.е. если бы у нас здесь в файле hosts/testserver.sls содержались переменные с timezone, то они бы имели преимущество.

В файлах top.sls все указывается без расширения .sls.


Работа с salt states

Приступим к простенькому state:

/srv/saltstack/salt/packages.sls:

# Install some basic packages for Debian systems
{% if grains['os_family'] == 'Debian' %}
basepackages:
  pkg.installed:
    - pkgs:
      - lsof
      - sysstat
      - telnet
{% endif %}

Как видно, здесь мы применили и jinja и grains и собственно сам pkg модуль.

Попробуем применить этот state в тестовом режиме:

root@saltsshbox:/srv/saltstack# salt-ssh testserver state.sls packages test=true
[INFO    ] Fetching file from saltenv 'base', ** done ** 'packages.sls'
testserver:
  Name: basepackages - Function: pkg.installed - Result: Differs

Summary for testserver
------------
Succeeded: 1 (unchanged=1)
Failed:    0
------------
Total states run:     1

И далее уже в реальном:

root@saltsshbox:/srv/saltstack# salt-ssh testserver state.sls packages
[INFO    ] Fetching file from saltenv 'base', ** skipped ** latest already in cache 'salt://packages.sls'
testserver:
  Name: basepackages - Function: pkg.installed - Result: Changed

Summary for testserver
------------
Succeeded: 1 (changed=1)
Failed:    0
------------
Total states run:     1


Salt Pillar или переменные

Следующее важное звено — это Pillar. Так в SaltStack называется переменные, или всё что задается со стороны мастера для удаленных систем. Частично они уже вам знакомы из описанного выше, поэтому сразу к делу.

Получить все pillar переменные, определенные для хоста:

root@saltsshbox:~# salt-ssh testserver pillar.items
testserver:
    ----------
    chrony:
        ----------
        lookup:
            ----------
            custom:
                # some custom addons
                # if you need it
    timezone:
        ----------
        name:
            Europe/Moscow

Также как и с Grains можно запросить отдельно взятую переменную:

salt-ssh testserver pillar.get 'timezone:name'


Использование state вместе с pillar

Рассмотрим следующий state:

/srv/saltstack/salt/timezone.sls:

{%- set timezone = salt['pillar.get']('timezone:name', 'Europe/Dublin') %}
{%- set utc = salt['pillar.get']('timezone:utc', True) %}
timezone_settings:
  timezone.system:
    - name: {{ timezone }}
    - utc: {{ utc }}

Здесь мы задали переменную на основе данных из pillar. Причем в этой конструкции:

{%- set timezone = salt['pillar.get']('timezone:name', 'Europe/Dublin') %}

Europe/Dublin — это значение по умолчанию, если по каким-то причинам salt не сможет получить значение из Pillar.


Выполним:
root@saltsshbox:/srv/saltstack# salt-ssh testserver state.sls timezone
[INFO    ] Fetching file from saltenv 'base', ** skipped ** latest already in cache 'salt://timezone.sls'
testserver:
  Name: Europe/Moscow - Function: timezone.system - Result: Changed

Summary for testserver
------------
Succeeded: 1 (changed=1)
Failed:    0
------------
Total states run:     1


Real Life пример

И вот, наконец-то, мы добрались уже до реального жизненного примера. Рассмотрим state синхронизации времени — chrony. Находится он у нас здесь:

/srv/saltstack/salt/chrony/init.sls

Причем init.sls это индекс по умолчанию, salt ищет его автоматически, но можно использовать и любой другой файл.

Здесь мы введем еще одну типичную конструкцию для salt — это map.jinja.

/srv/saltstack/salt/chrony/map.jinja:

{% set chrony = salt['grains.filter_by']({
  'RedHat': {
    'pkg': 'chrony',
    'conf': '/etc/chrony.conf', 
    'service': 'chronyd',
  },
  'Debian': {
    'pkg': 'chrony',
    'conf': '/etc/chrony/chrony.conf', 
    'service': 'chrony',
  },
}, merge=salt['pillar.get']('chrony:lookup')) %}

Её назначение — это создать необходимый набор статических переменных под нашу систему, но с возможностью слияния с переменными из pillar, если таковые вдруг понадобится указать.

Дальше сам /srv/saltstack/salt/chrony/init.sls:

{% from "chrony/map.jinja" import chrony with context %}

chrony:
  pkg.installed:
    - name: {{ chrony.pkg }}
  service:
    - name: {{ chrony.service }}
    - enable: True
    - running
    - require:
      - pkg: {{ chrony.pkg }}
      - file: {{ chrony.conf }}

{{ chrony.conf }}:
  file.managed:
    - name: {{ chrony.conf }}
    - source: salt://chrony/files/chrony.conf.jinja
    - template: jinja
    - user: root
    - group: root
    - mode: 644
    - watch_in:
      - service: {{ chrony.service }}
    - require:
      - pkg: {{ chrony.pkg }}

Здесь отдельного внимания заслуживает шаблон salt://chrony/files/chrony.conf.jinja формата jinja.

/srv/saltstack/salt/chrony/files/chrony.conf.jinja:

# managed by SaltStack
{%- set config = salt['pillar.get']('chrony:lookup', {}) -%}
{%- set vals = {
  'bindcmdaddress': config.get('bindcmdaddress','127.0.0.1'),
  'custom': config.get('custom', ''),
}%}

### chrony conf
server 0.centos.pool.ntp.org iburst                                              
server 1.centos.pool.ntp.org iburst                                              
server 2.centos.pool.ntp.org iburst                                              
server 3.centos.pool.ntp.org iburst                                              
stratumweight 0                                                                  
driftfile /var/lib/chrony/drift                                                  
rtcsync                                                                          
makestep 10 3                                                                    
bindcmdaddress {{ vals.bindcmdaddress }}
bindcmdaddress ::1                                                               
keyfile /etc/chrony.keys                                                         
commandkey 1                                                                     
generatecommandkey                                                               
noclientlog                                                                      
logchange 0.5                                                                    
logdir /var/log/chrony 

{% if vals.custom -%}                                                                                                                                          
{{ vals.custom }}                                               
{%- endif %}

В этом шаблоне мы также запрашиваем переменные из Pillar и обрабатываем их. Просмотреть как этот state воспринялся salt можно при помощи state.show_sls:


Вывод salt-ssh testserver state.show_sls chrony
root@saltsshbox:/srv/saltstack# salt-ssh testserver state.show_sls chrony
[INFO    ] Fetching file from saltenv 'base', ** done ** 'chrony/init.sls'
[INFO    ] Fetching file from saltenv 'base', ** done ** 'chrony/map.jinja'
testserver:
    ----------
    /etc/chrony/chrony.conf:
        ----------
        __env__:
            base
        __sls__:
            chrony
        file:
            |_
              ----------
              name:
                  /etc/chrony/chrony.conf
            |_
              ----------
              source:
                  salt://chrony/files/chrony.conf.jinja
            |_
              ----------
              template:
                  jinja
            |_
              ----------
              user:
                  root
            |_
              ----------
              group:
                  root
            |_
              ----------
              mode:
                  644
            |_
              ----------
              watch_in:
                  |_
                    ----------
                    service:
                        chrony
            |_
              ----------
              require:
                  |_
                    ----------
                    pkg:
                        chrony
            - managed
            |_
              ----------
              order:
                  10002
    chrony:
        ----------
        __env__:
            base
        __sls__:
            chrony
        pkg:
            |_
              ----------
              name:
                  chrony
            - installed
            |_
              ----------
              order:
                  10001
        service:
            |_
              ----------
              name:
                  chrony
            |_
              ----------
              enable:
                  True
            - running
            |_
              ----------
              require:
                  |_
                    ----------
                    pkg:
                        chrony
                  |_
                    ----------
                    file:
                        /etc/chrony/chrony.conf
            |_
              ----------
              order:
                  10000
            |_
              ----------
              watch:
                  |_
                    ----------
                    file:
                        /etc/chrony/chrony.conf

Дальше уже просто выполним его:

root@saltsshbox:/srv/saltstack# salt-ssh testserver state.sls chrony
testserver:
  Name: chrony - Function: pkg.installed - Result: Changed
  Name: /etc/chrony/chrony.conf - Function: file.managed - Result: Changed
  Name: chrony - Function: service.running - Result: Changed

Summary for testserver
------------
Succeeded: 3 (changed=3)
Failed:    0
------------
Total states run:     3

Здесь salt докладывает о 3х выполненных state по суммарному колличеству задействованных модулей. Если выполнить повторно, то видно что никаких изменений не было произведено:

root@saltsshbox:/srv/saltstack# salt-ssh testserver state.sls chrony
testserver:

Summary for testserver
------------
Succeeded: 3
Failed:    0
------------
Total states run:     3

Сразу же можно и посмотреть как сформировался конфигурационный файл для chrony:

salt-ssh testserver cmd.run 'cat /etc/chrony/chrony.conf'

Финально стоит упомянуть еще команду state.highstate.

salt-ssh testserver state.highstate

Она применяет все прописанные state для нашего тестового сервера.


Заключение

Итак, мы узнали чтоже из себя представляет salt-ssh из комплекта SaltStack и как его использовать. Узнали ключевые особенности построения окружения, необходимого для работы salt-ssh. Настроили тестовую среду при помощи Vagrant. И планомерно провели эксперименты с фундаментальными концепциями SaltStack, такими как: Grains, States, Pillar. Также мы узнали как писать state от простого к сложному, дойдя до реальных примеров, позволяющих на своей базе уже строить дальнейшую автоматизацию.

На этом пока все. За бортом осталось еще множество интересных тем, но я надеюсь что эта информация поможет начать работать с этой замечательной системой управления конфигурациями.

Полезная информация:

best_practices
walkthrough
starting_states
pillar
formulas
tutorials

© Habrahabr.ru