Как любой действующий администратор я столкнулся c задачей установки ПО и обновления этого ПО на управляемых мною хостах. Когда хостов было до 20 и они все были одного класса (Железо, OS …) с этим хорошо справлялись ssh скрипты, но к сожалению хостов становилось все больше и больше, к тому же стали появляться хосты с другими OS и хосты разошлись по версиям, например: где-то RHEL7, где-то RHEL9, добавились Debian подобные хосты, плюс к этому появились Windows терминальные сервера и сервера на FreeBSD. Обслуживать этот зоопарк руками стало слишком затратно по времени.
Раньше я использовал expect, но поскольку безопасность его зарубила, начал искать другие утилиты которые мне могли помочь. Перебрал кучу разных программ, среди них Puppet, Cheef, Jenkins (последний пытался приспособить для моих нужд, хоть он и не для того). В итоге остановился на Ansible – он кроссплатформенный (можно развернуть мастер на любой Unix системе), коннетиться может к любой OS (для Unix нужен рабочий SSH и подходящая версия Python >= 2.7.5, для Windows поднятый WinRM – но об этом позднее), не нужен клиент на стороне управляемого хоста.
2. Постановка задачи.
Итак, у меня есть 6 серверов:
1. Ansible-master – OS = OEL9.1 4CPU 8Gb RAM – 50G HDD (можно и меньше просто у меня такие типовые VM)
2. Ans-client01 – OS = OEL9.1 4CPU 8Gb RAM – 50G HDD (нода под nginx)
3. Ans-client02 – OS = OEL9.1 4CPU 8Gb RAM – 50G HDD (нода под nginx)
4. Ans-client03 – OS = OEL9.1 4CPU 8Gb RAM – 50G HDD (нода под haproxy)
5. Ans-client04 – OS = Ubuntu 22.04 (LTS) 4CPU 8Gb RAM – 50G HDD (нода под nginx)
6. Ans-client-win - OS = Windows Server 2022 4CPU 8Gb RAM – 50G HDD (нода под терминальный сервер)
Соответственно нужно на нодах развернуть nginx и скопировать туда мой сайт, к HaProxy и к Windows только приконнектимся, т.к. на HaProxy та же самая OS что и на большенстве машин.
Имя хоста IP адрес
Ansible-master 192.168.1.15
Ans-client01 192.168.1.16
Ans-client02 192.168.1.17
Ans-client03 192.168.1.19
Ans-client04 192.168.1.20
Ans-client-win 192.168.1.21
3. Знакомство с Ansible.
Ansible – свободно распространяемы продукт с открытым исходным кодом. Сайт документации https://docs.ansible.com
Документация по подключению Windows нод к Ansible
https://docs.ansible.com/ansible/latest ... x%EF%83%81
PowerShell скрипт автоконфигурации Window для работы с Ansible
https://github.com/ansible/ansible/blob ... nsible.ps1
Итак, Ansible. Как обычно начинаем с установки.
Для этого под рутом, включаем репозиторий ol9_EPEL
Код: Выделить всё
vim /etc/yum.repos.d/oracle-linux-ol9.repo
[ol9_EPEL]
name=Oracle Linux 9 EPEL ($basearch)
baseurl=https://yum$ociregion.$ocidomain/repo/OracleLinux/OL9/developer/EPEL/$basearch/
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-oracle
gpgcheck=1
enabled=0
Код: Выделить всё
[ol9_EPEL]
name=Oracle Linux 9 EPEL ($basearch)
baseurl=https://yum$ociregion.$ocidomain/repo/OracleLinux/OL9/developer/EPEL/$basearch/
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-oracle
gpgcheck=1
enabled=1Код: Выделить всё
yum install ansible vim-ansibleКод: Выделить всё
sudo yum install ansible vim-ansibleЗаводим пользователя ansible с длиннющим паролем (который нам все равно не будет нужен, коннектиться к нему будем по ключу)
Код: Выделить всё
useradd -c “Ansible User” -G wheel ansible
echo arhweurhgewhrgyg38yterbgh34tgfeqrh|passwd --stdin ansible
chage -m 0 -M 99999 ansibleНу и поднимаемся до пользователя ansible т.к. работать от рута не комильфо
Код: Выделить всё
su - ansibleКод: Выделить всё
ssh-keygenесли у вас на хостах есть подобный пользователь, то можете добавить ключ из /home/ansible/.ssh/id_rsa.pub в .ssh/authorized_keys этого пользователя
У меня клиентские хосты сделаны из шаблона, где уже заведен пользователь ansible и ему предоставлены права sudo c опцией NOPASSWD: и ключ уже лежит там /home/ansible/.ssh/authorized_keys
Во вторую очередь создаем каталог для хранения наших скриптов (playbook - в терминах ansible)
Код: Выделить всё
mkdir ansibleКод: Выделить всё
cd ansibleКод: Выделить всё
pwdКод: Выделить всё
/home/ansible/ansibleТеперь подробнее об Ansible
Ansible оперирует хостами заведенными в группы, которые находятся в инвентарном файле (файл может называться как угодно: hosts, groups, inventory…) мы на него просто ссылаемся, или явно когда у нас нет конфигурационного файла, или прописываем его в файле ansible.cfg, который должен лежать в корне папки с проектом, в нашем случае это папка /home/ansible/ansible
Ansible может функционировать в двух режимах
1. Мы с консоли вводим команды (режим Ad-Hok), команды запускаются следующим образом
Код: Выделить всё
ansible -i {inventory_file} {hostname} -m {module}{inventory_file} = Файл, где записаны группы и хосты на которых надо что-то исполнять
{hostname} = Хост или группа хостов где нужно выполнить команду
{module} = команда, которую надо выполнить
2. Скриптовой режим, когда мы запускаем не отдельную команду, а сценарий (режим Playbook), запускается следующим образом
Код: Выделить всё
ansible-playbook {yml_file.yml}{yml_file.yml} – файл с нашими скриптами и переменными в формате YML (это важно, т.к. ansible очень чувствителен к синтаксису не используйте табуляторы отделяйте уровень строки пробелями – количество пробелов не ограничено но каждая команда должна попадать на свое место)
Список команд можно посмотреть командой
Код: Выделить всё
ansible-doc -lСписок модулей и их версии можно посмотреть командой
Код: Выделить всё
ansible-galaxy collection listНачнем с подготовки inventory файла с описанием наших машин.
Итак, мы находимся в каталге /home/ansible/ansible, в нем создадим файл inventory
Код: Выделить всё
touch inventoryКод: Выделить всё
[ngx_srv]
ans-client01 ansible_host=192.168.1.16
ans-client02 ansible_host=192.168.1.17
ans-client04 ansible_host=192.168.1.20
[ha_srv]
ans-client03 ansible_host=192.168.1.18
[win_srv]
Ans-client-win ansible_host=192.168.1.21
[ngx_srv:vars]
ansible_user=ansible
ansible_ssh_private_key_file=/home/ansible/.ssh/id_rsa
ansible_python_interpreter=/usr/bin/python
[ha_srv:vars]
ansible_user=ansible
ansible_ssh_private_key_file=/home/ansible/.ssh/id_rsa
ansible_python_interpreter=/usr/bin/python
[win_srv: vars]
ansible_user=ansible
ansyble_port=5986
ansible_connection=winrm
ansible_winrm_server_cert_validation=ignoreКод: Выделить всё
ansible -i inventory ans-client03 -m pingКод: Выделить всё
ans-client03 | SUCCESS => {
"changed": false,
"ping": "pong"
}Имена групп указываются в квадратных скобках [Group_Name] при этом дефис в имени группы (да и вообще в переменных) не приветствуется и может приводить к ошибкам, поэтому рекомендую использовать нижнее подчеркивание
Хосты указываются следующим образом:
Можно указать просто IP
Код: Выделить всё
[tst_grp]
12.34.56.78[tst_grp]
test-srv01 и дальше переменные ansible
В итоге должно получится следующее
Код: Выделить всё
[tst_grp]
test-srv01 ansible_host=12.34.56.78 ansible_user=ansible ansible_ssh_private_key_file=/home/ansible/.ssh/id_rsa ansible_python_interpreter=/usr/bin/pythonКод: Выделить всё
[ngx_srv:vars]
ansible_user=ansible
ansible_ssh_private_key_file=/home/ansible/.ssh/id_rsa
ansible_python_interpreter=/usr/bin/python
[ha_srv:vars]
ansible_user=ansible
ansible_ssh_private_key_file=/home/ansible/.ssh/id_rsa
ansible_python_interpreter=/usr/bin/pythonКод: Выделить всё
cd /home/ansible/ansible/group_vars
touch ha_srv ngx_srvКод: Выделить всё
vim ngx_srvКод: Выделить всё
---
ansible_user : ansible
ansible_ssh_private_key_file : /home/ansible/.ssh/id_rsa
ansible_python_interpreter : /usr/bin/pythonКод: Выделить всё
vim ha_srvКод: Выделить всё
---
ansible_user : ansible
ansible_ssh_private_key_file : /home/ansible/.ssh/id_rsa
ansible_python_interpreter : /usr/bin/python1. Указывать все переменные среды для ansible напротив каждого хоста в отдельности (на мой вкус слишком много букв, не люблю много писать)
2. Вынести все общие переменные в отдельные блоки в инвернтори файле (когда групп не много это предпочтительно, imho) [group:vars]
3. Вынести все общие переменные в отдельные файлы в каталоге group_vars (предпочтительно для больших проектов)
Я остановился на второй стратегии итого инвентори файл у меня выглядит как я привел его выше.
Также группы можно помешать в другие группы это делается следующим образом
В нашем inventory есть 2 группы с Linux хостами [ngx_srv] [ha_srv] и мы их можем объединить в общую группу
Создаем группу Linux_srv c расширением children
Код: Выделить всё
[Linux_srv:children]
ngx_srv
ha_srvТак же следует учесть, что сервера не добавленные ни в какие группы (в начале файла можно указать просто ip адреса) входят в группу по умолчанию «ungrouped».
Есть еще одна группа по умолчанию “all” куда входят все сервера нашего inventory файла включая сервера без групп
Посмотреть inventory cо всеми переменными можно командой
Код: Выделить всё
ansible-inventory --listПереходим в каталог ansible cd /home/ansible/ansible
Создаем там файл ansible.cfg следующего содержания
Код: Выделить всё
vim ansible.cfgКод: Выделить всё
[defaults]
host_key_checking = false
inventory = ./inventory
interpreter_python = auto_legacy_silent Посмотрим, что для чего
host_key_checking = false – не спрашивать фингерпринт а сразу его принимать
inventory = ./inventory – указываем расположение нашего инвентори файла
interpreter_python = auto_legacy_silent – не выводить ошибку если версия python отличается
Теперь мы можем использовать сокращенные команды вроде
ansible all -m ping
Настройка окружения ansible закончена, можно переходить к написанию нашего сценария
5. Создаем сценарий (playbook) Ansible для разворачивания приложений на серверах.
Как я писал ранее сценарий ansible это yml файл который имеет несколько особенностей
1. Он начинается всегда с --- в первой строке
2. Каждая команда начинается с «- »
3. Каждый параметр пишется четко под командой или с дополнительным пробелом
Например:
Код: Выделить всё
---
- name: Install nginx # название нашего сценария
hosts: ngx_srv # хосты на которых выполнять этот сценарий
become: yes # выполнять команды от рута
vars: # Переменные для нашего сценария
src_fold: /ansible/html/files/static
tmp_fold: /ansible/html/files/templ
dst_fold: /usr/share/nginx/html
bkp_fold: /usr/share/nginx/html
bkp_file: /usr/share/nginx/bkphtml.tgz
tasks: # говорит сценарию что ниже идут команды
- name: Check Linux Distribution # Название задания
debug: var=ansible_os_family # Вывести на экран переменную ansible “ansible_os_family”
- block: # ============== Block for RedHat
- name: Install Nginx
yum: name=nginx state=latest
when: ansible_os_family == "RedHat"
…Есть полезная команда
Код: Выделить всё
ansible all -m setupТеперь немного о операторах которые используются в сценариях ansible
1. - name – всегда определяет название такса, или playbook в целом
2. - hosts – указывается 1 раз на playbook и определяет на каких хостах будет исполняться сценарий
3. – become – опционально – говорит о том будет ли наш сценарий отрабатывать с правами суперпользователя или нет. По умолчанию значение “no”
4. vars: – блок в котором определяются переменные имеет 2 типа записи
4.1. vars: var1=variable1 var2=variable2
4.2. vars:
var1: variable1
var2: variable2
по мне второй вариант предпочтительнее
5. tasks: – блок в котором определяются наши задания
Например
Код: Выделить всё
tasks: # говорит сценарию что ниже идут команды
- name: Check Linux Distribution # Название задания
debug: var=ansible_os_family # Вывести на экран переменную ansible “ansible_os_family”Код: Выделить всё
tasks: # говорит сценарию что ниже идут команды
- debug: var=ansible_os_family # Вывести на экран переменную ansible6. Команды debug, set_fact, register используются для получения обратной связи при выполнении сценария
debug – вывод значения переменной, например:
Код: Выделить всё
debug: var=var1Код: Выделить всё
debug:
var: var1Код: Выделить всё
debug:
msg: “Znachenie peremennoy var1 = {{ var1 }}”Например
Код: Выделить всё
set_fact: full_var=”{{ var1 }} {{ var2 }}”
debug: var=full_varКод: Выделить всё
debug:
var: full_varregister – служит для перехвата вывода исполняемой команды, например
Код: Выделить всё
- shell: uptime
register: result
- debug:
var: result7. block, when
- block служит для групприровки команд выполняемых на серверах, например: у нас есть разные сервера с разными OS (redhat debian gentoo …) и у них разные пакетные менеджеры для того, чтобы не получать ошибку при инсталляции nginx надо разделить эти сервера по определенному признаку. Признак задается указанием ключевого слова when: в блоке, например:
Код: Выделить всё
- block: # ============== Block for RedHat
- name: Install Nginx
yum: name=nginx state=latest
when: ansible_os_family == "RedHat"
- block: # ============== Block for Debian
- name: Install Nginx to Debian
apt: name=nginx state=latest
when: ansible_os_family == "Debian"when: может использоваться и сам по себе, когда конкретный таск должен быть выполнен только если есть подходящие для этого условия, например
Код: Выделить всё
- name: Install Nginx
yum: name=nginx state=latest
when: ansible_os_family == "RedHat"допускается так же инверсия выбора
Код: Выделить всё
when: ansible_os_family != "RedHat"8. В ansible как и в любом другом языке есть операторы цикла loop (он же with_items), until, with_fileglob
loop очень удобен когда надо поставить много пакетов или скопировать часть файлов из папки, например
Код: Выделить всё
- name: Install Packeges
yum: name={{ item }} state=latest
loop:
- nginx
- tar
- mc
- vimпример использования
Код: Выделить всё
- name: Copy new static file
copy: src={{ item }} dest={{ dst_fold }} mode=644 owner=nginx
with_fileglob: "{{ src_fold }}/*"
пример использования
Код: Выделить всё
- name: Loop Until
shell: echo -n Z >> myfile.txt && cat myfile.txt
register: out
delay: 2
retries: 10
until: out.stdout.find("ZZZZ") == false
- name: Print OUT.STDOUT
debug: var=out.stdout9. Темплейты Jinja
Используются для кастомизации файлов которые должны упасть на разные ноды
В файл который надо кастомизировать добавляются переменные ansible и при копировании файла на целевую ноду, они заменяются на значения которые есть в переменных (переменные должны быть определены в сценарии или вычисляться на лету, но на момент копирования должны уже иметь значения)
Использование:
У нас есть файл который представляет собой html страницу
Код: Выделить всё
<HTML>
<HEAD>
<TITLE>Test Ansible Pege</TITLE>
<SCRIPT LANGUAGE="JavaScript">
var sizes = new Array(0,1,2,4,8,10,12,14,16)
sizes.pos = 0
function Elastic()
{
var el = document.all.elastic
if (null == el.direction)el.direction = 1
else if ((sizes.pos > sizes.length - 2 ) || (0 == sizes.pos))
el.direction *= -1
el.style.letterSpacing = sizes[sizes.pos += el.direction]
setTimeout('Elastic()',75)
}
</SCRIPT>
<body bgcolor="black" onload=Elastic()>
<CENTER>
<br><br><br><br><br><br><br><br>
<font color="green"><h2>This Html Page Generated by</h1>
<font color="gold"><H1 id=elastic ALIGN="Center">ANSIBLE</H1>
<br><br><br><br><br><br><br><br>
<font color="yellow">Owner of this Server is: {{ owner }}<br>
<br><br><br><br>
<font color="white">Server Hostname is: {{ ansible_hostname }}<br>
<font color="white">Server OS is: {{ ansible_distribution }} {{ ansible_distribution_version }}<br>
<font color="white">Server IP Address is: {{ ansible_default_ipv4.address }}<br>
</body>
</html>
{{ owner }} {{ ansible_hostname }} {{ ansible_distribution }} {{ ansible_distribution_version }} и {{ ansible_default_ipv4.address }}
Переменная owner унаследована от хоста из файла inventory, остальные принадлежат самому клиенту на котором будет выполняться сценарий
В плейбуке это будет выглядеть так
Код: Выделить всё
- name: Generate index.html
template: src={{ tmp_file }} dest={{ dst_fold }} mode=644 owner=nginx
notify: Restart Nginx{{ tmp_file }} – расположение самого темплейта
{{ dst_fold }} – папка на клиенте куда надо положить нужный файл
notify – вызывает хендлер по имени
10. Хендлеры
Хендлеры это куски кода сценария, которые сами по себе не исполняются, но могут быть вызваны из другой части сценария, служат если вызывающая часть сценария произвела изменение на клиенте, например заменила конфигурацию сервиса и надо сделать перезапуск, например
Код: Выделить всё
- name: Generate index.html
template: src={{ tmp_file }} dest={{ dst_fold }} mode=644 owner=nginx
notify: Restart Nginx
handlers:
- name: Restart Nginx
service: name=nginx state=restartednotify: Restart Nginx
ansible ищет хендлер с названием «Restart Nginx» и запускает его.
11. Перенаправление выполнения задачи на определенный сервер с помощью параметра delegate_to и run_once
Допустим у нас есть 3 инстанса DB и нам надо обновить одну из схем, делать на всех 3 серверах это бессмысленно при этом нам без разницы на каком из 3 серверов нам это делать. Для этого используется run_once:
Пример
Код: Выделить всё
- name: Run command on random server ( run command on 1 server from group )
shell: echo "Run Command"
run_once: trueДля выполнения той же команды на определенном сервере (например на Ans-client0) команда будет выглядеть так
Код: Выделить всё
- name: Run command on server Ans-client01
shell: echo "Run Command"
delegate_to: Ans-client01например после обновления нам надо перезагрузить сервер и подождать когда он вернется чтобы выполнить проверку
делается это следующим образом
Код: Выделить всё
- name: Reboot my Servers
shell: sleep 3 && reboot now
async: 1
poll: 0
- name: Wait till my servers com up online
wait_for:
host: "{{ inventory_hostname }}"
state: started
delay: 5
timeout: 40
delegate_to: 127.0.0.1Дальше сценарий передает управление хосту ansible-master 127.0.0.1 и тот ждет возвращения хоста в течении 40 секунд после чего можно запускать проверочные задачи
Теперь как обещал готовый playbook
ansible.cfg
Код: Выделить всё
[defaults]
host_key_checking = false
inventory = ./inventory
interpreter_python = auto_legacy_silentКод: Выделить всё
[ngx_srv]
ans-client01 ansible_host=192.168.1.16 owner=admin
ans-client02 ansible_host=192.168.1.17 owner=engeneer
ans-client04 ansible_host=192.168.1.20 owner=ciso
[ha_srv]
ans-client03 ansible_host=192.168.1.19 owner=devopsКод: Выделить всё
cat group_vars/ngx_srvКод: Выделить всё
---
ansible_user : ansible
ansible_ssh_private_key_file : /home/ansible/.ssh/id_rsa
ansible_python_interpreter : /usr/bin/python
Код: Выделить всё
cat group_vars/ha_srvКод: Выделить всё
---
ansible_user : ansible
ansible_ssh_private_key_file : /home/ansible/.ssh/id_rsa
ansible_python_interpreter : /usr/bin/pythonКод: Выделить всё
cat nginx.ymlКод: Выделить всё
---
- name: Install nginx
hosts: ngx_srv
become: yes
vars:
src_fold: "/ansible/html/files/static/"
tmp_fold: /ansible/html/files/templ
dst_fold: /usr/share/nginx/html
bkp_fold: /usr/share/nginx/html
time: "{{ lookup('pipe', 'date +\"%d%m%Y\"') }}"
bkp_file: /usr/share/nginx/{{ time }}bkphtml.tgz
tasks:
- name: Check Linux Distribution
debug: var=ansible_os_family
- block: # ============== Block for RedHat
- name: Install Nginx and Rsync to RedHat
yum: name={{ item }} state=latest
loop:
- nginx
- rsync
when: ansible_os_family == "RedHat"
- block: # ============== Block for Debian
- name: Install Nginx and Rsync to Debian
apt: name={{ item }} state=latest
loop:
- nginx
- rsync
when: ansible_os_family == "Debian"
- name: Start and Enable Nginx
service: name=nginx state=started enabled=yes
- name: Check Nginx Version
shell: nginx -v
register: vers
- name: Print installed nginx version
debug: var=vers.stderr
- name: Backup old index.html file
archive:
path: "{{ bkp_fold }}"
dest: "{{ bkp_file }}"
- name: Copy new static file
synchronize: src={{ src_fold }} dest={{ dst_fold }}
notify: Restart Nginx
- name: Generate index.html
template: src={{ tmp_fold }}/index.html dest={{ dst_fold }} mode=644 owner=nginx
notify: Restart Nginx
handlers:
- name: Restart Nginx
service: name=nginx state=restarted
