Мне очень понравилось, как было просто конфигурировать виртуальную машину с Ansible. Вот теперь думаю, а что, если машин было бы несколько? Ведь оригинальный пример был о нескольких машинах: один Consul сервер и два рабочих агента. Сервер уже готов, так что будет интересно довести пример до конца. Так что приступим.
И да, логически этот пост очень зависит от предыдущего, так что если что-то кажется бессмыслицей, то это либо было раскрыто в прошлом посте, либо я где-то накосячил, что тоже случается.
Что у нас уже есть
За прошлый пост мы успели сделать вот что:
- Vagrantfile, чтобы создать виртуальную машину,
- инвентарный файл для Ansible, чтобы знать, где она находится,
- Ansible плейбук, чтобы её настроить,
- заголовок systemd сервиса для Консула и
- файл конфигурации init.json.j2 для него же.
Чтобы не прыгать между постами и гитхабом туда и обратно, вот тела всех перечисленных файлов (много, много букв).
1 2 3 4 5 6 7 8 9 |
Vagrant.configure("2") do |config| config.vm.box = "ubuntu/xenial64" config.vm.define "consul-server" do |machine| machine.vm.network "private_network", ip: "192.168.99.100" machine.vm.provision "shell", inline: "apt-get update && apt-get install -y python-minimal" machine.vm.provision "ansible", playbook: "consul.yml" end end |
1 |
consul-server ansible_host=192.168.99.100 ansible_user=ubuntu ansible_ssh_pass=ubuntu |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
- hosts: consul-server vars: consul_version: 0.9.2 consul_server_ip: 192.168.99.100 consul_config_dir: /etc/systemd/system/consul.d tasks: - name: Install unzip apt: name=unzip state=present become: true - name: Install Consul become: true unarchive: src: https://releases.hashicorp.com/consul/{{ consul_version }}/consul_{{ consul_version }}_linux_amd64.zip remote_src: yes dest: /usr/local/bin creates: /usr/local/bin/consul mode: 0555 - name: Make Consul a service become: true copy: src: consul.service dest: /etc/systemd/system/consul.service - name: Ensure config directory exists become: true file: path: "{{ consul_config_dir }}" state: directory - name: Deploy consul config become: true template: src: init.json.j2 dest: "{{consul_config_dir}}/init.json" - name: Ensure consul's running become: true service: name=consul state=started |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[Unit] Description=consul agent Requires=network-online.target After=network-online.target [Service] EnvironmentFile=-/etc/sysconfig/consul Restart=on-failure ExecStart=/usr/local/bin/consul agent $CONSUL_FLAGS -config-dir=/etc/systemd/system/consul.d ExecReload=/bin/kill -HUP $MAINPID [Install] WantedBy=multi-user.target |
1 2 3 4 5 6 7 8 |
{ "server": true, "ui": true, "advertise_addr": "{{ consul_server_ip }}", "client_addr": "{{ consul_server_ip }}", "data_dir": "/tmp/consul", "bootstrap_expect": 1 } |
Надеюсь, вы это просто проскролили. Как и в прошлый раз, vagrant up
создаст и подготовит новую виртуалку, на которой будет жить готовый Consul сервер. Но пока мы этого делать не будем, потому что сначала нужно создать ещё парочку хостов.
Шаг 0. Добавим ещё виртуальных машин
Vagrant всё-таки прекрасен. Без него пришлось бы бездумно прокликивать менюшки в VirtualBox и много ждать, а тут пара строк кода, и машины будут готовы меньше, чем за минуту.
Код в Vagrantfile, который создавал consul-server
VM, уже тогда был подозрительно похож на функцию. Я думаю, что похожесть стоит оформить окончательно, и потом уже переиспользовать эту функцию и для остальных машин. Плюс ко всему, пока Ansible плейбук ничего не знает о остальных хостах, его стоит убрать из Vagrantfile и чуть что, запускать руками. Правда, из-за этого придётся снова вернуть строку, которая настраивала SSH пользователя.
В конечном итоге вот, что у меня получилось:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Vagrant.configure("2") do |config| config.vm.box = "ubuntu/xenial64" def create_consul_host(config, hostname, ip) config.vm.define hostname do |host| host.vm.hostname = hostname host.vm.network "private_network", ip: ip host.vm.provision "shell", inline: "echo ubuntu:ubuntu | chpasswd" host.vm.provision "shell", inline: "apt-get update && apt-get install -y python-minimal" end end create_consul_host(config, "consul-server", "192.168.99.100") create_consul_host(config, "consul-host-1", "192.168.99.101") create_consul_host(config, "consul-host-2", "192.168.99.102") end |
Функция create_consul_host
создаст машину полностью готовую к тому, чтобы на неё натравили Ansible, и мы вызываем её три раза, чтобы получить три идентичные машины: consul-server
, consul-host-1
and consul-host-2
. vagrant up
вдохнёт в них жизнь, и я даже не буду проверять, всё ли с ними в порядке. Конечно, всё ОК.
Шаг 1. Учим Ansible доверять
Если быстренько попытаться отправить новым хостам какую-нибудь Ansible команду (вроде ansible all -i hosts -m ping
), то Ansible так же быстренько откажется её выполнять. Хосты-то незнакомые, откуда ему знать, что им можно верить. В прошлый раз мне пришлось руками подтверждать, что верить хосту можно, но делать такое регулярно определённо задолбает. Нужно что-то более перманентное. Например, добавить опцию в настройки Ansible и заставить его верить всем подряд.
Оказывается, просто добавив ansible.cfg
в текущую папку и положив туда всего одну настройку, можно начисто отключить у Ansible критическое мышление:
1 2 |
[defaults] host_key_checking = False |
В моём случае, правда, пришлось дополнительно сходить в ~/.ssh/known_hosts
и удалить оттуда записи об айпишках 192.168.99.100-102
. Я их использовал раньше, и если бы Ansible заметил, что за старым адресом прячется новый хост, ему бы крышу снесло.
Итак, виртуальные машины запущены, Ansible — сама доверчивость, так что можно запустить какой-нибудь ping и любоваться, как все три хоста счастливо отвечают pong:
1 2 3 4 5 |
ansible all -i hosts -m ping #consul-server | SUCCESS => { # "changed": false, # "ping": "pong" #} |
Хм, отозвался только consul-server. Но с другой стороны, чему удивляться. Инвентарный файл ведь старый остался, он только про constul-server
и знает. Бывает.
Шаг 2. Добавляем новые хосты в инвентарный файл
1 |
consul-server ansible_host=192.168.99.100 ansible_user=ubuntu ansible_ssh_pass=ubuntu |
В первоначальном hosts
файле была всего одна строчка с айпишкой и паролем, так что, скопипастив её раза два, можно было бы добавить в него и остальные хосты. Но блин, получится как-то слишком уродливо. К тому же, у хостов будут разные роли, кто-то сервер, кто-то клиент, так что стоит это как-то обозначить на инвентарном уровне.
Попробуем-ка организовать хосты по группам. Например, consul-server
будет единственным представителем группы servers
, consul-host-1
и -2
будут принадлежать группе nodes
, а обе группы будут включены в родительскую группу cluster
. Ну и к тому же общие логины и пароли можно задать прямо на уровне группы и тем самым избежать греха копипасты.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
consul-server ansible_host=192.168.99.100 consul-host-1 ansible_host=192.168.99.101 consul-host-2 ansible_host=192.168.99.102 [servers] consul-server [nodes] consul-host-[1:2] [cluster:children] servers nodes [cluster:vars] ansible_user=ubuntu ansible_ssh_pass=ubuntu |
Выглядит сурово и солидно. Как в продакшене. Маска consul-host-[1:2]
в середине файла особенно удалась. Она и строку текста экономит, и подчёркивает продвинутость автора. Наверное.
Зато теперь, если пингануть все (all)
хосты, то всё будет очень, очень хорошо:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
ansible all -i hosts -m ping #consul-host-1 | SUCCESS => { # "changed": false, # "ping": "pong" #} #consul-server | SUCCESS => { # "changed": false, # "ping": "pong" #} #consul-host-2 | SUCCESS => { # "changed": false, # "ping": "pong" #} |
Вместо all
, кстати, можно было бы подставить как имена наших групп, так и имена отдельных хостов.
Итак, с настройками покончили, идём писать плейбук.
Шаг 3. Адаптируем плейбук под новые роли
В прошлый раз мы настраивали consul-server
в шесть шагов:
- Установить unzip.
- Установить Consul.
- Сделать Consul сервисом.
- Убедиться, что папка для настроек существует.
- Положить настройки консула.
- Запустить consul, если нужно.
В мире Консулов их поведение определяется файлом настроек, поэтому только пятая задача будет меняться под конкретные роли. Всё остальное же будет одинаковое как для сервера, так и для обычных работяг.
Так как в один плейбук можно положить сразу несколько сценариев (у нас пока был только один), то настройку всего кластера можно сделать в четыре подхода:
- Установить Consul сервисы на все VM (шаги 1-4).
- Положить конфигурацию Consul-сервера (шаг 5).
- Положить конфигурацию Consul-нодов (шаг 5).
- Запустить Consul-агенты на всех VM (шаг 6).
Шаг 3.1. Устанавливаем Consul-сервисы
Будем кромсать существующий код. Первый сценарий, в принципе, это же старый consul.yml
целиком минус несколько вещей:
- «Положить настройки консула» и «Запустить консул, если нужно» шаги пока не нужны. Удаляем.
- Вместо хоста
consul-server
, мы теперь настраиваем группуcluster
(первая строка). - Переменная
consul_server_ip
пока не нужна. Сжигаем. - Сам по себе Consul недавно обновили, так что в переменную
consul_version
(четвёртая строка) кладём новую версию —0.9.3
.
Получившийся consul.yml
теперь выглядит вот так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- hosts: cluster vars: consul_version: 0.9.3 consul_config_dir: /etc/systemd/system/consul.d tasks: - name: Install unzip apt: name=unzip state=present become: true # ... - name: Ensure config directory exists become: true file: path: "{{ consul_config_dir }}" state: directory |
Так как хосты consul-server
, consul-host-1
и -2
всё ещё запущены, то установить Консул на их все можно одной несчастной командой:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
ansible-playbook -i hosts consul.yml # #PLAY [cluster] *********************************************************** # #TASK [Gathering Facts] *************************************************** #ok: [consul-server] #ok: [consul-host-1] #ok: [consul-host-2] # ... #PLAY RECAP *************************************************************** #consul-host-1 : ok=5 changed=4 unreachable=0 failed=0 #consul-host-2 : ok=5 changed=4 unreachable=0 failed=0 #consul-server : ok=5 changed=4 unreachable=0 failed=0 |
И это срабатывает реально быстро. Секрет в том, что Ansible обслуживает хосты одновременно.
Шаг 3.2. Настраиваем consul-server
Тут можно было бы просто скопипастить кусок старого года, но я думаю, что хоть что-то стоит сделать правильно.
Во-первых, посмотрим на задачу, которую нам нужно добавить:
1 2 3 4 5 |
- name: Deploy consul config become: true template: src: init.json.j2 dest: "{{consul_config_dir}}/init.json" |
init.json.j2
файл, название которого имело смысл при конфигурации одного хоста, оный смысл начинает терять сейчас, когда их несколько. Это файл для Консул-сервера? Для клиентов? А вот переименуем его в server.init.json.j2
, и всякая неопределённость отвалится сама собой.
Идём дальше. Наша задача ссылается на переменную consul_config_dir
, которая вообще-то была определена в предыдущем сценарии и поэтому тут невидима. Можно было бы, конечно, объявить её повторно, но это опять будет грех копипасты. Лучше уж мы её сделаем глобальной переменной, перенеся в инвентарный файл.
1 2 3 4 5 |
;... [cluster:vars] ansible_user=ubuntu ansible_ssh_pass=ubuntu consul_config_dir=/etc/systemd/system/consul.d |
Есть ещё один момент: шаблон server.init.json.j2
использует переменную consul_server_ip
, которую тоже надо где-то объявить. Но эта переменная даже в прошлом посте казалось избыточной. Серверная айпишка и так ведь была объявлена и в Vagrantfile, и в hosts
, и теперь ещё и в consul.yml
. Сколько же можно.
С другой стороны, если мы смогли положить consul_config_dir
в инвентарный файл, то, может, оттуда можно и взять чего-нибудь. Например, ansible_host
, в котором айпишка хоста и так есть.
1 2 3 4 5 6 7 8 |
{ "server": true, "ui": true, "advertise_addr": "{{ ansible_host }}", "client_addr": "{{ ansible_host }}", "data_dir": "/tmp/consul", "bootstrap_expect": 1 } |
В результате получается вот такой вот второй сценарий в нашем consul.yml
:
1 2 3 4 5 6 7 8 9 10 11 12 |
- hosts: cluster #.... - hosts: servers tasks: - name: Deploy consul server config become: true template: src: server.init.json.j2 dest: "{{consul_config_dir}}/init.json" |
Если кто-то забыл, то servers
одна из групп, которые мы объявили в hosts
.
ansible-playbook -i hosts consul.yml
в свою очередь не сделает никакой новой магии, кроме как положит JSON конфигурацию на единственную машину с Consul сервером.
Шаг 3.3. Настраиваем Consul агентов
Это будет интересно. Сам по себе файл настройки агента даже проще, чем у сервера. Назовём его client.init.json.j2
и будем готовиться отправлять на хосты. Но есть одно «но».
1 2 3 4 5 |
{ "advertise_addr": "{{ ansible_host }}", "retry_join": ["{{ consul_server_ip }}"], "data_dir": "/tmp/consul" } |
Хотя мы и можем раздобыть айпишку самого агента, снова обратившись к переменной ansible_host
, где теперь искать айпишку сервера, consul_server_ip
? Не объявлять же её снова в плейбуке.
Оказывается, есть немного чёрной магии, которая может помочь и тут. Вы когда-нибудь задумывались, что означает мистическая строка «TASK [Gathering Facts]», которая постоянно мелькает в выводе ansible-playbook
? Ansible, в своей доброте душевной, по-умолчанию выполняет неявный таск, который собирает информацию о хостах, прежде чем их надругать плейбуком. Он выясняет и переменные окружения, и версию операционки, и сетевые интерфейсы. Много чего выясняет. Что ещё здорово, эта информация будет потом сгруппирована по тем же группам, что и хосты в hosts
. И её можно использовать! Нужно всего-то найти хоть один хост в группе servers
и взять его адрес.
Переменная, в которую стекаются эти данные называется hostvars
, и вот, как её можно использовать:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- hosts: cluster #... - hosts: servers #... - hosts: nodes tasks: - set_fact: consul_server={{ hostvars[inventory_hostname]['groups']['servers'][0] }} - set_fact: consul_server_ip={{ hostvars[consul_server]['ansible_all_ipv4_addresses'][0] }} - name: Deploy consul client config become: true template: src: client.init.json.j2 dest: "{{consul_config_dir}}/init.json" |
Всё колдунство происходит в строках 9 и 10. Сначала создаём переменную (факт) consul_server
, в которую кладём первый хост из группы servers
, а во вторую переменную — consul_server_ip
— кладём его айпишку. Всё просто же. Узнать, что ещё можно было бы вытянуть из hostvars
можно, например, добавив в плейбук задачу debug
. Например, - debug: var=hostvars
.
Шаг 3.4. Запускаем все консул-сервисы
Вообще ерунда:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- hosts: cluster # ... - hosts: servers # ... - hosts: nodes # ... - hosts: cluster tasks: - name: Ensure consul's running become: true service: name=consul state=started |
Теперь, запустив плейбук целиком, мы получим готовый для экспериментов Консул кластер по адресу 192.168.99.100:8500:
Шаг 3.5. Запускаем плейбук из Vagrantfile
Тут придётся подумать и, возможно, пойти на компромиссы. В прошлом посте, когда vagrant настраивал один хост, он очень удобно генерировал свой собственный инвентарный файл, и тем самым решал проблемы и с поиском айпишки, и с конфигурацией SSH. Сейчас же, когда в инвентарнике у нас есть и группы, и переменные, Вагрант скорее накосячит, чем сделает удобство. К счастью, через inventory_path
ему можно передать уже существующий файл, так что одна проблема решается просто.
Вторая проблема тоже растёт из дефолтных настроек вагранта. В отличие от ansible-playbook
, который работает со всеми хостами параллельно, вагрантовский провижинер будет настраивать хосты по одному. Это не только неэффективно, но и ломает нашу формулу поиска айпишки сервера — consul_server_ip
. Если в данный момент настраивается только, например, consul-host-1
, то в hostvars
кроме него никого и не будет.
Но опять же, есть выход. В ansible провиженере есть опция под названием limit
, и если ей поставить "all"
, то все хосты будут обрабатываться параллельно. Вот как это выглядит по итогу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# ... def create_consul_host(config, hostname, ip) config.vm.define hostname do |host| #... host.vm.provision "shell", inline: "apt-get update && apt-get install -y python-minimal" yield host if block_given? end end create_consul_host(config, "consul-server", "192.168.99.100") create_consul_host(config, "consul-host-1", "192.168.99.101") create_consul_host(config, "consul-host-2", "192.168.99.102") do |host| host.vm.provision "ansible" do |ansible| ansible.limit = "all" ansible.inventory_path = "./hosts" ansible.playbook = "consul.yml" end end # ... |
Теперь, как и в прошлом посте, vagrant up
создаст и настроит всё, от начала и до конца.
Мораль
Как оказалось, настраивать кластер при помощи Ansible не намного тяжелее, чем одну машину. По ощущениям, вообще всё то же самое. Да, плейбук стал побольше, да и инвентарный файл потолстел, но весь процесс тем не менее остался внятным.
Я особенно счастлив, что выяснил, как динамически находить айпишку consul_server_ip
. Ну вот вообще доволен. Конечно, для полного счастья, нужно чтобы и в инвентарном файле вообще не осталось адресов, и все текущие настройки шли исключительно от Vagrant, но для начала и это неплохо.
Все сырцы для этого поста можно найти на github.
Спасибо за труд. Очень помогла статья