Ansible
В данном практическом занятии рассматривается базовое использование ansible.
Vagrant
Для работы с ansible воспользуемся следующим Vagrantfile
c тремя машинами:
Vagrant.configure("2") do |config|
config.vm.define "bastion" do |c|
c.vm.box = "ubuntu/lunar64"
c.vm.hostname = "bastion"
c.vm.network "private_network", type: "dhcp"
c.vm.provision "shell", inline: <<-SHELL
apt-get update -q
apt-get install -yq libnss-mdns ansible
SHELL
end
config.vm.define "node1" do |c|
c.vm.box = "ubuntu/lunar64"
c.vm.hostname = "node1"
c.vm.network "private_network", type: "dhcp"
c.vm.provision "shell", inline: <<-SHELL
apt-get update -q
apt-get install -yq libnss-mdns
SHELL
end
config.vm.define "node2" do |c|
c.vm.box = "ubuntu/lunar64"
c.vm.hostname = "node2"
c.vm.network "private_network", type: "dhcp"
c.vm.provision "shell", inline: <<-SHELL
apt-get update -q
apt-get install -yq libnss-mdns
SHELL
end
end
Все управление будем производить с машины bastion
, для взаимодействия с другими машинами
потребуется ssh
ключ, путь до которого можно узнать в выводе команды vagrant ssh-config
:
$ vagrant ssh-config bastion | grep IdentityFile
IdentityFile ~/.vagrant.d/boxes/ubuntu-VAGRANTSLASH-lunar64/0/virtualbox/vagrant_insecure_key
Скопируем данный файл в директорию проекта с Vagrantfile
с именем key
.
Все дальнейшие команды будем вводить находясь на машине bastion
, в первую очередь добавив
ключ пользователю vagrant
и выставив переменную ANSIBLE_HOST_KEY_CHECKING
в значение
False
для отключения проверки ssh
ключей.
$ install -m 600 -o vagrant /vagrant/key /home/vagrant/.ssh/id_rsa
$ export ANSIBLE_HOST_KEY_CHECKING=False
Inventory
Для управления другими машинами нам необходимо создать inventory файл с их списком,
который также можно разбить по группам. Создадим файл hosts
:
[bastion]
bastion.local
[nodes]
node1.local
node2.local
Basic Usage
Управлять машинами из inventory можно из командной строки с помощью команды ansible
.
С помощью опции -i
указывается путь до inventory, --key-file
указывает на ssh
ключ
с которым будет производиться подключение, -m
задает модуль для исполнения и позиционный
аргумент (в данном случае all
) выбирает на каких машинах будет происходить выполнение.
Для отключения проверки ssh
ключей при подключении можно выставить переменную среды
ANSIBLE_HOST_KEY_CHECKING
в значение False
. Воспользуемся модулем ping
и
запустим проверку всех машин:
$ export ANSIBLE_HOST_KEY_CHECKING=False
$ ansible -i hosts -m ping all
bastion.local | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
node1.local | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
node2.local | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
Можно также выбрать группу для запуска, если не требуется запуск на всех машинах. Воспользуемся
модулем shell
для запуска команды на группе nodes
:
$ ansible -i hosts -m shell -a 'uname -a' nodes
node1.local | CHANGED | rc=0 >>
Linux node1 6.2.0-31-generic #31-Ubuntu SMP PREEMPT_DYNAMIC Mon Aug 14 13:42:26 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
node2.local | CHANGED | rc=0 >>
Linux node2 6.2.0-31-generic #31-Ubuntu SMP PREEMPT_DYNAMIC Mon Aug 14 13:42:26 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
Также ограничить выполнения только определенными хостами/группами можно опцией -l|--limit
:
$ ansible -i hosts -m shell -a 'hostname' nodes -l node1.local
node1.local | CHANGED | rc=0 >>
node1
Playbook
Tasks
Для декларативного описания задач(tasks), которые будут выполняться на машинах в inventory,
можно описать playbook в виде yaml
файла. Плейбук может состоять из одного или нескольких play
,
которые в свою очередь содержат задачи(tasks). Также в play
указывается группа хостов(hosts)
для запуска. Сделаем плейбук, который будет устанавливать пакет nginx
на группу nodes
с
помощью модуля ansible.builtin.package
:
- hosts: nodes
become: true
tasks:
- name: nginx package
ansible.builtin.package:
name: nginx
Здесь также добавлен параметр become: true
для запроса повышения привилегий, так как для установки
пакетов в систему требуются права root
. Сохраним в файл playbook.yaml
и запустим с помощью
команды ansible-playbook
:
$ ansible-playbook -i hosts playbook.yaml
PLAY [nodes] *******************************************************************************************
TASK [Gathering Facts] *********************************************************************************
ok: [node1.local]
ok: [node2.local]
TASK [nginx package] ***********************************************************************************
changed: [node1.local]
changed: [node2.local]
PLAY RECAP *********************************************************************************************
node1.local : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2.local : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
$ curl -s node1.local | grep title
<title>Welcome to nginx!</title>
$ curl -s node2.local | grep title
<title>Welcome to nginx!</title>
Как видно ansible
запускает play
и в нем запускает ряд задач tasks
. Первым запускается
задача Gathering Facts
, которая собирает информацию о машинах во внутренние переменные, которые
потом можно использовать в других задачах. Если информация не нужна, то можно добавить параметр
gather_facts: False
:
---
- hosts: nodes
become: True
gather_facts: False
tasks:
- name: nginx package
ansible.builtin.package:
name: nginx
$ ansible-playbook -i hosts playbook.yaml
PLAY [nodes] *******************************************************************************************
TASK [nginx package] ***********************************************************************************
ok: [node1.local]
ok: [node2.local]
PLAY RECAP *********************************************************************************************
node1.local : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2.local : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Как видно задача по сбору фактов пропала, а также видно что задача nginx package
теперь находится
не в статусе changed
, а в статусе ok
, что означает что никаких действий не было произведено, так
как пакет был установлен при прошлом запуске.
Handlers
Для того чтобы запускать какие-либо действия только при изменениях в play
также
можно указать список handlers
, задачи в котором будут выполняться только при наступлении
определенных событий.
Добавим в основной раздел tasks
задачу по копированию конфигурации, а в handlers
рестарт
сервиса nginx
, который будет запускаться при изменении конфигурации:
- hosts: nodes
become: True
gather_facts: False
tasks:
- name: nginx package
ansible.builtin.package:
name: nginx
- name: nginx config
ansible.builtin.copy:
src: default
dest: /etc/nginx/sites-enabled/default
notify: nginx restart
handlers:
- name: nginx restart
ansible.builtin.systemd:
name: nginx
state: restarted
Сам файл конфигурации расположим рядом с плейбуком в директории files
, которая используется
по умолчанию для копирования файлов на удаленные машины. Файл конфигурации будет называться
default
:
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
root /var/www/html;
index index.html;
}
}
$ ansible-playbook -i hosts playbook.yaml
PLAY [nodes] *****************************************************************************
TASK [nginx package] *********************************************************************
ok: [node1.local]
ok: [node2.local]
TASK [nginx config] **********************************************************************
changed: [node1.local]
changed: [node2.local]
RUNNING HANDLER [nginx restart] **********************************************************
changed: [node1.local]
changed: [node2.local]
PLAY RECAP *******************************************************************************
node1.local : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2.local : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Jinja2
В ansible
используются шаблонизатор jinja2
для использования переменных как в самих
плейбуках, так и в модуле template
для шаблонизации файлов на удаленных машинах.
Создадим в директории templates
шаблон файл index.html.j2
:
hello from {{ ansible_host }}
Где ansible_host
будет указывать на машину, на которой будет выполняться задача. Дополним плейбук:
- hosts: nodes
become: True
gather_facts: False
vars:
html_dir: /var/www/html
tasks:
- name: nginx package
ansible.builtin.package:
name: nginx
- name: nginx config
ansible.builtin.copy:
src: default
dest: /etc/nginx/sites-enabled/default
notify: nginx restart
- name: index.html to "{{ html_dir }}"
ansible.builtin.template:
src: index.html.j2
dest: "{{ html_dir }}/index.html"
handlers:
- name: nginx restart
ansible.builtin.systemd:
name: nginx
state: restarted
Здесь мы также вынесли в блок с переменными vars
путь до html директории, чтобы показать как
можно использовать переменные в самом плейбуке.
$ ansible-playbook -i hosts playbook.yaml
PLAY [nodes] ******************************************************************************************
TASK [nginx package] **********************************************************************************
ok: [node1.local]
ok: [node2.local]
TASK [nginx config] ***********************************************************************************
ok: [node1.local]
ok: [node2.local]
TASK [index.html to "/var/www/html"] ******************************************************************
changed: [node1.local]
changed: [node2.local]
PLAY RECAP ********************************************************************************************
node1.local : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2.local : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
$ curl node1.local
hello from node1.local
$ curl node2.local
hello from node2.local
Conditionals
Для задачи можно определить условия, при которых она должна выполняться с помощью
директивы when
:
---
- hosts: nodes
become: True
gather_facts: False
vars:
html_dir: /var/www/html
tasks:
- name: nginx package
ansible.builtin.package:
name: nginx
- name: nginx config
ansible.builtin.copy:
src: default
dest: /etc/nginx/sites-enabled/default
notify: nginx restart
- name: index.html to "{{ html_dir }}"
ansible.builtin.template:
src: index.html.j2
dest: "{{ html_dir }}/index.html"
- name: node1only file
ansible.builtin.copy:
content: "hello from node1 only\n"
dest: "{{ html_dir }}/node1only"
when: ansible_host == "node1.local"
handlers:
- name: nginx restart
ansible.builtin.systemd:
name: nginx
state: restarted
$ ansible-playbook -i hosts playbook.yaml
PLAY [nodes] ******************************************************************************************
TASK [nginx package] **********************************************************************************
ok: [node2.local]
ok: [node1.local]
TASK [nginx config] ***********************************************************************************
ok: [node2.local]
ok: [node1.local]
TASK [index.html to "/var/www/html"] ******************************************************************
ok: [node1.local]
ok: [node2.local]
TASK [node1only file] *********************************************************************************
skipping: [node2.local]
changed: [node1.local]
PLAY RECAP ********************************************************************************************
node1.local : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
$ curl node1.local/node1only
hello from node1 only
$ curl node2.local/node1only
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.22.0 (Ubuntu)</center>
</body>
</html>
Как видно задача на машине node2.local
не выполнялась, так как для нее не выполнено условие в
директиве when
.
Loops
В ansible
есть несколько директив для определения цикла: loop
, with_*
, until
.
С помощью них можно выполнить задачу множество раз с разными переменными.
Добавим задачу с циклом в плейбук:
---
- hosts: nodes
become: True
gather_facts: False
vars:
html_dir: /var/www/html
tasks:
- name: nginx package
ansible.builtin.package:
name: nginx
- name: nginx config
ansible.builtin.copy:
src: default
dest: /etc/nginx/sites-enabled/default
notify: nginx restart
- name: index.html to "{{ html_dir }}"
ansible.builtin.template:
src: index.html.j2
dest: "{{ html_dir }}/index.html"
- name: node1only file
ansible.builtin.copy:
content: "hello from node1 only\n"
dest: "{{ html_dir }}/node1only"
when: ansible_host == "node1.local"
- name: loop files
ansible.builtin.copy:
content: "{{ ansible_host }}\n"
dest: "{{ html_dir}}/{{ item }}"
loop:
- test1
- test2
- test3
handlers:
- name: nginx restart
ansible.builtin.systemd:
name: nginx
state: restarted
$ ansible-playbook -i hosts playbook.yaml
PLAY [nodes] ******************************************************************************************
TASK [nginx package] **********************************************************************************
ok: [node2.local]
ok: [node1.local]
TASK [nginx config] ***********************************************************************************
ok: [node1.local]
ok: [node2.local]
TASK [index.html to "/var/www/html"] ******************************************************************
ok: [node1.local]
ok: [node2.local]
TASK [node1only file] *********************************************************************************
skipping: [node2.local]
ok: [node1.local]
TASK [loop files] *************************************************************************************
changed: [node1.local] => (item=test1)
changed: [node2.local] => (item=test1)
changed: [node1.local] => (item=test2)
changed: [node2.local] => (item=test2)
changed: [node1.local] => (item=test3)
changed: [node2.local] => (item=test3)
PLAY RECAP ********************************************************************************************
node1.local : ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2.local : ok=4 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
$ curl node1.local/test1
node1.local
$ curl node2.local/test2
node2.local
$ curl node1.local/test3
node1.local