Salt Beacons/Reactors and IaC

В данном практическом занятии опробуем механизмы обратной связи в saltstack такие как beacons и reactors. А также попробуем реализовать подход IaC(Infrastructure as Code), развернув собственный git репозиторий и настроив salt на автоматическое применение конфигурации из него.

Vagrant

Для работы с salt будем использовать следующий Vagrantfile:

Vagrant.configure("2") do |config|
  config.vm.define "master" do |c|
    c.vm.box = "ubuntu/lunar64"
    c.vm.hostname = "master"
    c.vm.network "private_network", type: "dhcp"
    c.vm.network "forwarded_port", guest: 3000, host: 3000
    c.vm.provision "shell", inline: <<-SHELL
      curl -fsSL -o /etc/apt/keyrings/salt-archive-keyring-2023.gpg \
        https://repo.saltproject.io/salt/py3/ubuntu/22.04/amd64/SALT-PROJECT-GPG-PUBKEY-2023.gpg
      echo "deb [signed-by=/etc/apt/keyrings/salt-archive-keyring-2023.gpg arch=amd64] https://repo.saltproject.io/salt/py3/ubuntu/22.04/amd64/latest jammy main" \
        | tee /etc/apt/sources.list.d/salt.list
      apt-get update -q
      apt-get install -yq libnss-mdns salt-master salt-minion
      echo 'master: master.local' > /etc/salt/minion.d/master.conf
      systemctl enable --now salt-master.service
      systemctl restart salt-minion.service
      systemctl enable salt-minion.service
    SHELL
  end

  config.vm.define "minion1" do |c|
    c.vm.box = "ubuntu/lunar64"
    c.vm.hostname = "minion1"
    c.vm.network "private_network", type: "dhcp"
    c.vm.provision "shell", inline: <<-SHELL
      curl -fsSL -o /etc/apt/keyrings/salt-archive-keyring-2023.gpg \
        https://repo.saltproject.io/salt/py3/ubuntu/22.04/amd64/SALT-PROJECT-GPG-PUBKEY-2023.gpg
      echo "deb [signed-by=/etc/apt/keyrings/salt-archive-keyring-2023.gpg arch=amd64] https://repo.saltproject.io/salt/py3/ubuntu/22.04/amd64/latest jammy main" \
        | tee /etc/apt/sources.list.d/salt.list
      apt-get update -q
      apt-get install -yq libnss-mdns salt-minion
      echo 'master: master.local' > /etc/salt/minion.d/master.conf
      systemctl restart salt-minion.service
      systemctl enable salt-minion.service
    SHELL
  end

  config.vm.define "minion2" do |c|
    c.vm.box = "ubuntu/lunar64"
    c.vm.hostname = "minion2"
    c.vm.network "private_network", type: "dhcp"
    c.vm.provision "shell", inline: <<-SHELL
      curl -fsSL -o /etc/apt/keyrings/salt-archive-keyring-2023.gpg \
        https://repo.saltproject.io/salt/py3/ubuntu/22.04/amd64/SALT-PROJECT-GPG-PUBKEY-2023.gpg
      echo "deb [signed-by=/etc/apt/keyrings/salt-archive-keyring-2023.gpg arch=amd64] https://repo.saltproject.io/salt/py3/ubuntu/22.04/amd64/latest jammy main" \
        | tee /etc/apt/sources.list.d/salt.list
      apt-get update -q
      apt-get install -yq libnss-mdns salt-minion
      echo 'master: master.local' > /etc/salt/minion.d/master.conf
      systemctl restart salt-minion.service
      systemctl enable salt-minion.service
    SHELL
  end
end

Команды будут выполняться на машине master из под пользователя root. Для этого после команды vagrant ssh можно выполнить команду sudo -i.

Accept Keys

После того как машины загрузятся необходимо принять ключи от миньонов:

# salt-key -A
The following keys are going to be accepted:
Unaccepted Keys:
master
minion1
minion2
Proceed? [n/Y]
Key for minion master accepted.
Key for minion minion1 accepted.
Key for minion minion2 accepted.

State

Опишем простое состояние с пакетом nginx и файлом index.html, для этого создадим файл /srv/salt/top.sls:

base:
  'minion*':
  - nginx

А также сам файл /srv/salt/nginx.sls:

nginx:
  pkg.installed: []

/var/www/html/index.html:
  file.managed:
  - contents: |
      hello from {{ grains['id'] }}

После чего применим состояние:

# salt 'minion*' state.apply
minion1:
----------
...
------------
Succeeded: 2 (changed=2)
Failed:    0
------------
Total states run:     2
Total run time:  11.861 s

# curl minion1.local
hello from minion1
# curl minion2.local
hello from minion2

Beacon

Добавим на миньоны beacon, который будет отслеживать изменения файла index.hml и отправлять информацию об этом на мастер. Для этого добавим в файл состояния /srv/salt/nginx.sls описание beacon:

nginx:
  pkg.installed: []

pyinotify:
  pip.installed: []

/var/www/html/index.html:
  file.managed:
  - contents: |
      hello from {{ grains['id'] }}

beacon_index:
  beacon.present:
  - save: True
  - enable: True
  - files:
      /var/www/html/index.html:
        mask:
        - modify
  - disable_during_state_run: True
  - beacon_module: inotify

После чего применим состояние на миньонах:

# salt 'minion*' state.apply
minion1:
----------
...

Summary for minion1
------------
Succeeded: 4 (changed=1)
Failed:    0
------------
Total states run:     4
Total run time:   7.135 s

Для проверки работы beacon можно на мастере запустить команду

# salt-run state.event pretty=True

И в отдельном окне терминала зайти на миньон и изменить файл index.html:

# echo 123 > /var/www/html/index.html

После чего в терминале мастера в выводе команды salt-run state.event получим событие:

# salt-run state.event pretty=True
salt/beacon/minion1/beacon_index//var/www/html/index.html       {
    "_stamp": "2023-11-05T12:51:23.283037",
    "change": "IN_MODIFY",
    "id": "minion1",
    "path": "/var/www/html/index.html"
}

Таким образом мы убедились, что мастер получает требуемые нам события от миньонов.

Reactor

С помощью механизма reactors можно вызывать функции при получении событий. Опишем реакцию на получения событий от миньонов таким образом, что при получении события применялось текущее состояние, описанное в наших sls файлах. Для этого дополним конфигурацию мастера параметром reactor, создав файл /etc/salt/master.d/reactor.conf:

reactor:
- salt/beacon/*:            # события
  - /srv/reactor/apply.sls  # reactor файл

Здесь мы описываем при каких события какой reactor файл sls будет вызван. Создадим сам файл /srv/reactor/apply.sls:

apply:
  local.state.apply:        # функция, local - исполняется на мастере
  - tgt: '{{ data['id'] }}' # на каких миньонах выполнять

В котором мы описываем функцию на исполнение, а также указываем на каких миньонах как в команде salt. Здесь используется шаблон jinja2, который берет информацию из полученного события в переменной data, где id будет имя миньона. Таким образом при получении события по изменению файла index.html будет принудительно применено состояние, которое нами было описано ранее.

Для применения новой конфигурации мастера необходимо перезапустить сервис:

# systemctl restart salt-master.service

Проверим, изменив файл index.html на миньоне и подождав некоторое время:

# echo 123 > /var/www/html/index.html
# curl minion1.local;sleep 5;curl minion1.local
123
hello from minion1

GitFS

Salt позволяет хранить состояния не только на локальной файловой системе, он также поддерживает различные бекенды для хранения. Одним из бекендов может выступать git репозиторий. Поднимем собственный git репозиторий на мастере, для этого опишем состояние для него в файле /srv/salt/master.sls:

gitea:
  pkg.installed:
  - name: docker.io

  pip.installed:
  - pkgs:
    - docker
    - pygit2

  docker_container.running:
  - image: gitea/gitea
  - port_bindings:
    - 3000:3000

Данным состоянием мы описываем запуск контейнера с образом gitea. Добавим это состояние на мастер в файле /srv/salt/top.sls:

base:
  'minion*':
  - nginx
  'master':
  - master

И применим его:

# salt master state.apply
master:
----------
...

Summary for master
------------
Succeeded: 3 (changed=3)
Failed:    0
------------
Total states run:     3
Total run time:  53.592 s

После чего откроем в браузере страницу http://localhost:3000 с Initial Configuration, на которой можно не меняя параметры нажать кнопку Install Gitea. После этого можно создать себе аккаунт, а после и новый репозиторий в правом верхнем углу. Создадим репозиторий с именем salt, после чего получим пустой репозиторий:

Добавим все наши файлы из /srv/salt в новый репозиторий, для этого зайдем в эту директорию и инициализируем новый репозиторий, добавим все файлы и отправим в наш новый репозиторий:

# cd /srv/salt/
# git init
Initialized empty Git repository in /srv/salt/.git/
# git add .
# git commit -m 'init'
 3 files changed, 37 insertions(+)
 create mode 100644 master.sls
 create mode 100644 nginx.sls
 create mode 100644 top.sls
# git remote add origin http://localhost:3000/alex/salt.git
# git push -u origin master
Username for 'http://localhost:3000': alex
Password for 'http://alex@localhost:3000':
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 2 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 609 bytes | 609.00 KiB/s, done.
Total 5 (delta 0), reused 0 (delta 0), pack-reused 0
remote: . Processing 1 references
remote: Processed 1 references in total
To http://localhost:3000/alex/salt.git
 * [new branch]      master -> master
branch 'master' set up to track 'origin/master'.

Теперь добавим конфигурацию для мастера, чтобы он брал состояния из нашего репозитория, для этого добавим файл /etc/salt/master.d/git.conf:

fileserver_backend:
- gitfs

gitfs_remotes:
- http://localhost:3000/alex/salt.git

Для применения новой конфигурации мастера необходимо перезапустить сервис:

# systemctl restart salt-master.service

После чего более директория /srv/salt использоваться не будет, а состояние будет тянуться из нашего репозитория. Отредактируем файл состояния nginx.sls прямо через веб интерфейс http://localhost:3000, изменив контент для index.html:

nginx:
  pkg.installed: []

pyinotify:
  pip.installed: []

/var/www/html/index.html:
  file.managed:
  - contents: |
      hello from {{ grains['id'] }} with git

beacon_index:
  beacon.present:
  - save: True
  - enable: True
  - files:
      /var/www/html/index.html:
        mask:
        - modify
  - disable_during_state_run: True
  - beacon_module: inotify

По-умолчанию мастер обновляет состояние из репозитория раз в минуту, так что подождав некоторое время можно применить состояние:

# salt 'minion*' state.apply
minion1:
----------
...
Summary for minion1
------------
Succeeded: 4 (changed=1)
Failed:    0
------------
Total states run:     4
Total run time:   1.316 s
# curl minion1.local
hello from minion1 with git
# curl minion2.local
hello from minion2 with git

Schedule

Чтобы не применять состояние вручную можно также добавить запуск по расписанию:

# salt 'minion*' schedule.add apply_job function='state.apply' seconds=30
minion1:
    ----------
    changes:
        ----------
        apply_job:
            added
    comment:
        Added job: apply_job to schedule.
    result:
        True
minion2:
    ----------
    changes:
        ----------
        apply_job:
            added
    comment:
        Added job: apply_job to schedule.
    result:

Обновим контент для index.html в файле nginx.sls в репозитории:

nginx:
  pkg.installed: []

pyinotify:
  pip.installed: []

/var/www/html/index.html:
  file.managed:
  - contents: |
      hello from {{ grains['id'] }} with git by schedule

beacon_index:
  beacon.present:
  - save: True
  - enable: True
  - files:
      /var/www/html/index.html:
        mask:
        - modify
  - disable_during_state_run: True
  - beacon_module: inotify

После чего через некоторое время состояние на миньонах обновится без ручного применения состояния:

# curl minion1.local
hello from minion1 with git by schedule
# curl minion2.local
hello from minion2 with git by schedule