В своей предыдущей статье я уже упоминал, что создаю своё микрооблако. Точнее, ну как своё, это будет облако как продукт, а не как услуга. Поэтому оно такое же моё, как и ваше. Как и всего мира. То есть каждый сможет его развернуть на своё железо, будь то датацентр или пара-тройка виртуальных машин и пользоваться им в своё удовольствие, запуская на нем свои потрясающие проекты. Можно даже найти какой-то готовый проект, который лежит где-нибудь на гитхабе и давно просится на старт, только автору исходного кода запускать его было негде, потому что хранение исходников у нас бесплатное, а вот поддержка работы своего сервиса к дешевому сегменту экономики на мой взгляд не относится. А я такой запустил облако, вкинул туда чужой проект и помог миру увидеть то, что так и могло остаться в глубинах гитхаба. Но это фантазии, до реализации которых еще далеко, но путешествие в тысячу миль начинается с первого шага, главное не прекращать перебирать ногами. Поэтому мы продолжаем…
Так вот в прошлом посте в рамках проекта kubos мы остановились на том, что объединили все машинки нашего будущего облака в общую виртуальную сеть. Теперь пора сделать так, чтобы машинки видели друг друга не только по IP-адресам, но и по именам. Для этого нужно запустить и настроить DNS сервер, чем мы сейчас и займемся. Для этого я отвел в гитлабе отдельную ветку, где и будет приведен полный код ansible и не только скриптов, о которых пойдет речь ниже.
Собираем инфу о хостах
Для начала нужно с каждой машины в нашей сети получить информацию о том, какой ip-адрес ей был выдан при подключении в виртуальную сеть. Вероятно, я сделал это несколько костыльно, но не нашел ничего лучше, как на каждой машине создавать json-файл с описанием того, как ее зовут и какой ip-адрес ей выдан. Код ниже был добавлен в ансибл скрипты создания openvpn сети тут и тут
# подготовка реестра какому хосту какой IP был выдан при подключении к сети OpenVPN - name: Create {{inventory_hostname}}.json file shell: | VPN_IP=$(ip route | grep tun0 | grep -v via | sed 's/.* src \([0-9.]*\).*/\1/'); REVERSE_VPN_IP=$(echo $VPN_IP | awk -F . '{print $4"."$3"."$2"."$1}'); echo "{ \"vpn_hosts\": [ { \"name\": \"{{ inventory_hostname }}\", \"ip\": \"$VPN_IP\", \"reverse_ip\": \"$REVERSE_VPN_IP\" } ] }" > {{inventory_hostname}}.json; args: chdir: $HOME creates: "{{inventory_hostname}}.json" - name: Fetch {{inventory_hostname}}.json fetch: src: "$HOME/{{inventory_hostname}}.json" dest: "{{ playbook_dir }}/inventory/" flat: yes
Такой же код запускается и в скрипте запуска ансибл скриптов, то есть в файле entrypoint.sh. Напоминаю, что скрипт запуска ансибл скриптов мы запускаем из докер контейнера и этот контейнер становится частью виртуальной сети
# подготовка реестра какому хосту какой IP был выдан при подключении к сети OpenVPN VPN_IP=$(ip route | grep tun0 | grep -v via | sed 's/.* src \([0-9.]*\).*/\1/'); REVERSE_VPN_IP=$(echo $VPN_IP | awk -F . '{print $4"."$3"."$2"."$1}'); echo "{ \"vpn_hosts\": [ { \"name\": \"docker\", \"ip\": \"$VPN_IP\", \"reverse_ip\": \"$REVERSE_VPN_IP\" } ] }" > openvpn/inventory/docker.json;
Ниже приведен пример такого json-файла, который будет создан на каждой виртуальной машине и в докер-контейнере
{ "vpn_hosts": [ { "name": "worker1", "ip": "10.10.0.6", "reverse_ip": "6.0.10.10" } ] }
В качестве имени хоста используется имя хоста с точки зрения ансибла, ip — это адрес выданный в openvpn сети (он получен скриптом выше из виртуального устройства tun0). Также в этот же файл добавляется реверсный ip-адрес, т.к. он понадобится в таком виде при настройке реверсной DNS-зоны (так называется зона DNS, которая нужна для поиска имени хоста по его ip-адресу). Реверсную зону настраивать необязательно, если это и правда не нужно. Но я все же решил сделать полноценную настройку DNS, чтоб уж до конца во всем разобраться. Также обращаю внимание, что я специально в json создаю массив из одного объекта, чтобы дальше, получив все такие файлы со всем машин, их проще было смержить в один общий массив при помощи замечательной консольной утилиты jq.
Давайте как раз перейдем к рассмотрению этого мержинга в скрипте запуска всех плейбуков entrypoint.sh
# https://e.printstacktrace.blog/merging-json-files-recursively-in-the-command-line/ jq -s ' def deepmerge(a;b): reduce b[] as $item (a; reduce ($item | keys_unsorted[]) as $key (.; $item[$key] as $val | ($val | type) as $type | .[$key] = if ($type == "object") then deepmerge({}; [if .[$key] == null then {} else .[$key] end, $val]) elif ($type == "array") then (.[$key] + $val) else $val end ) ); deepmerge({}; .)' openvpn/inventory/* > inventories.json;
Я тут не случайно указал ссылку над данным кодом, потому что для мержа json-ов мне пришлось воспользоваться помощью друга-интернета, потому что писать подобную функцию мне самому не хотелось, а задача, очевидно, уже была кем-то решена, что и подтвердилось после недолгой гуглежки. За данный код выражаю благодарность в адрес Szymon Stepniak. Я внес в этот код лишь небольшие почти косметические изменения, потому что код работал не совсем корректно.
По итогу отработки jq будет создан inventories.json файл вида
{ "vpn_hosts": [ { "name": "master", "ip": "10.10.0.1", "reverse_ip": "1.0.10.10" }, { "name": "worker1", "ip": "10.10.0.6", "reverse_ip": "6.0.10.10" }, { "name": "worker2", "ip": "10.10.0.8", "reverse_ip": "8.0.10.10" } ] }
Пора уже запустить этот паспортный стол
Для разворачивания DNS-сервера BIND создаем отдельный ансибл playbook
--- - hosts: master become: true become_user: root become_method: sudo roles: - dns-server - hosts: all,!master become: true become_user: root become_method: sudo roles: - dns-client
В данном плейбуке 2 роли: днс-сервер и днс-клиент. Этот плейбук как и ранее запускается из докера в скрипте entrypoint.sh при помощи следующего ряда команд
# подготовка файла инвентаризации хостов для настройки DNS { echo "[servers]"; echo "master ansible_host=host.docker.internal ansible_port=${MASTER_PORT} ansible_user=${USER_NAME} ansible_password=${USER_PASSWORD} ansible_sudo_pass=${USER_PASSWORD}"; echo ""; echo "[clients]"; echo "worker1 ansible_host=host.docker.internal ansible_port=${WORKER1_PORT} ansible_user=${USER_NAME} ansible_password=${USER_PASSWORD} ansible_sudo_pass=${USER_PASSWORD}"; echo "worker2 ansible_host=host.docker.internal ansible_port=${WORKER2_PORT} ansible_user=${USER_NAME} ansible_password=${USER_PASSWORD} ansible_sudo_pass=${USER_PASSWORD}"; echo "worker3 ansible_host=host.docker.internal ansible_port=${WORKER3_PORT} ansible_user=${USER_NAME} ansible_password=${USER_PASSWORD} ansible_sudo_pass=${USER_PASSWORD}"; echo "worker4 ansible_host=host.docker.internal ansible_port=${WORKER4_PORT} ansible_user=${USER_NAME} ansible_password=${USER_PASSWORD} ansible_sudo_pass=${USER_PASSWORD}"; } > dns/hosts; # установка DNS ansible-playbook -i dns/hosts dns/playbook.yml \ --extra-vars "dns_ip=$VIRTUAL_NETWORK_GATEWAY" \ --extra-vars "@inventories.json";
Сначала как обычно создается файл инвентаризации хостов для ансибла, а далее происходит самое главное: передача переменных в плейбук. Обратите внимание, что переменные передаются путем скармливания inventories.json файла, созданного выше после мержа.
Когда работает плейбук, то на днс-сервере сначала устанавливается сам bind, а потом происходит настройка его конфигов путем заполнения jinja2 шаблонов, которые так любят ансибл девопсы :). Сначала приведу эту часть скрипта ансибла, а потом разберем сами шаблоны
- name: Install bind9 apt: update_cache: yes name: [ 'bind9', 'bind9utils', 'bind9-doc' ] state: present register: bind9_installed - name: Replace named.conf.options template: src: named.conf.options.j2 dest: /etc/bind/named.conf.options when: bind9_installed.changed - name: Replace named.conf.local template: src: named.conf.local.j2 dest: /etc/bind/named.conf.local when: bind9_installed.changed - name: Ensure /etc/bind/zones directory exists file: path: /etc/bind/zones state: directory - name: Create db.host.name files template: src: db.host.name.j2 dest: "/etc/bind/zones/db.{{ item.name }}" with_items: "{{ vpn_hosts }}" when: bind9_installed.changed - name: Create db.host.ip files template: src: db.host.ip.j2 dest: "/etc/bind/zones/db.{{ item.ip }}" with_items: "{{ vpn_hosts }}" when: bind9_installed.changed
Тут всего 4 конфига, хотя не такое уж и «всего» :). Сначала создаются опции named.conf.options
acl trusted_clients { {% for vpn_host in vpn_hosts %} {{ vpn_host.ip }}; # {{ vpn_host.name }} {% endfor %} }; options { directory "/var/cache/bind"; allow-query { trusted_clients; }; forwarders { 8.8.8.8; 8.8.4.4; }; //======================================================================== // If BIND logs error messages about the root key being expired, // you will need to update your keys. See https://www.isc.org/bind-keys //======================================================================== dnssec-validation auto; listen-on-v6 { any; }; };
Тут из важного, это настройка ip адресов тех машин, которым разрешено пользоваться DNS-сервером (trusted_clients). В блоке acl данного шаблона идет перечисление всех ip-адресов всех машин в виртуальной сети. В блоке options данный acl подключается, как разрешенный, при помощи директивы allow-query. Также в блоке options важно задать раздел forwarders, чтобы для имен, которые не относятся к нашей виртуальной сети, поиск ip-адресов выполнялся на DNS-серверах google. Все остальное оставлено без изменений в том виде, в котором данный файл поставляется с bind9.
Далее рассмотрим базовый конфиг named.conf.local
{% for vpn_host in vpn_hosts %} // For {{ vpn_host.name }} zone "{{ vpn_host.name }}" { type master; file "/etc/bind/zones/db.{{ vpn_host.name }}"; # zone file path }; zone "{{ vpn_host.reverse_ip }}.in-addr.arpa" { type master; file "/etc/bind/zones/db.{{ vpn_host.ip }}"; # reverse zone file path for {{ vpn_host.name }} }; {% endfor %}
Для упрощения автоматизации заполнения данного шаблона будем создавать по одной зоне и реверсивной зоне для каждого хоста в виртуальной сети. Идеологически это не совсем корректно, потому что нужно всего 2 зоны: зона виртуальной сети и реверсивная зона виртуальной сети. Однако из-за сложности конфигов bind, особенно реверсивных конфигов, где нужно использовать реверсивные адреса, мы упростим себе жизнь, создав по одному файлу на каждое имя хоста и на каждый ip-адрес в виртуальной сети.
Теперь рассмотрим конфиг зоны db.host.name
; ; BIND data file ; $TTL604800 @INSOAlocalhost. root.localhost. ( 3; Serial 604800; Refresh 86400; Retry 2419200; Expire 604800 ); Negative Cache TTL ; ; name servers - NS records IN NS localhost. ; name servers - A records localhost. IN A {{ dns_ip }} ; {{ item.name }} - A records {{ item.name }}. IN A {{ item.ip }}
где важно в Serial поставить 3 вместо дефолтной 2-ки, чтобы bind при перезапуске увидел, что в файл внесены изменения. Также нужно заполнить 3 раздела:
-
NS-запись с локалхостом
-
A-запись, чтобы задать свой собственный ip, как ip DNS-сервера
-
A-запись, чтобы задать связь между именем и ip адресом конкретного хоста виртуальной сети
Такой файл будет создаваться для каждого виртуального хоста в сети.
И осталось рассмотреть конфиг реверсной зоны db.host.ip
; ; BIND reverse data file ; $TTL604800 @INSOAlocalhost. root.localhost. ( 3; Serial 604800; Refresh 86400; Retry 2419200; Expire 604800 ); Negative Cache TTL ; ; name servers IN NS localhost. ; PTR Records IN PTR {{ item.name }}. ; {{ item.ip }}
Тут с полем Serial и NS-записью все аналогично конфигу выше для не реверсивной зоны. Единственная разница, что вместо A-записи заполняется одна PTR-запись с именем привязанным к ip-адресу, для которого создается этот файл конфига.
Вернемся к ансибл скрипту с ролью днс-сервера. После того, как все шаблоны файлов конфигов заполнены, нужно проверить корректность конфигурации и рестартовать днс-сервер. Хорошо, что для проверки конфигов bind он поставляется вместе с такими утилитами, как named-checkconf и named-checkzone. Ими мы и воспользуемся
- name: Run named-checkconf shell: | named-checkconf; when: bind9_installed.changed - name: Run named-checkzone for zones shell: | named-checkzone {{ item.name }} /etc/bind/zones/db.{{ item.name }}; with_items: "{{ vpn_hosts }}" when: bind9_installed.changed - name: Run named-checkzone for reverse zones shell: | named-checkzone {{ item.reverse_ip }}.in-addr.arpa /etc/bind/zones/db.{{ item.ip }}; with_items: "{{ vpn_hosts }}" when: bind9_installed.changed - name: Restart named service: name: named state: restarted when: bind9_installed.changed
Проверять никогда не лишне
Теперь днс-сервер настроен верно и на этом можно заканчивать, но я предлагаю добавить в ансибл скрипты и скрипт запуска ансибл скриптов, который работает в докер контейнере, следующий код проверки корректности отработки всех настроек
- name: Check DNS working correct shell: | # проверить, что DNS настроен правильно if [ $(nslookup google.com | grep -c "Address:\s\+{{ dns_ip }}#53") != 1 ]; then echo "Using incorrect DNS server"; exit 1; fi if [ $(nslookup google.com | grep -c "** server can't find") != 0 ]; then echo "DNS not working"; exit 1; fi for (( index=0; index<$(echo "{{ vpn_hosts }}" | jq length); index++ )); do name=$(echo "{{ vpn_hosts }}" | jq -sr ".[0][$index].name"); ip=$(echo "{{ vpn_hosts }}" | jq -sr ".[0][$index].ip"); reverse_ip=$(echo "{{ vpn_hosts }}" | jq -sr ".[0][$index].reverse_ip"); echo "Check DNS for name=$name ip=$ip reverse_ip=$reverse_ip"; if [ $(nslookup $name | grep -c "Address: $ip") != 1 ]; then echo "DNS server's zones configured incorrectly"; exit 1; fi if [ $(nslookup $ip | grep -c "$reverse_ip.in-addr.arpa\s\+name = $name.") != 1 ]; then echo "DNS server's reverse zones configured incorrectly"; exit 1; fi done echo "DNS - OK"; echo "" > $HOME/dns.checked; args: creates: $HOME/dns.checked executable: /bin/bash # меняю, потому что /bin/sh не справится с for выше
В этом коде просто проверяется, что и интернет имена доступны на примере google.com, и что доступны имена всех наших машин. Для наших машин также проверяется и реверсивный днс-поиск.
Подводим итоги
Теперь у нас машины состоят в виртуальной сети и знают друг друга по именам.
Настало время устроить рок-н-ролл на этих машинах. На следующем этапе я планирую запустить на них кубер. Так шаг за шагом мы создадим что-то приличное в нашем микрооблаке.
И в завершении, как всегда, предлагаю присоединяться к нашему телеграмм каналу и чату. О всех крупных вехах развития проекта планируется создаваться публикации на хабре, но в канале будут публиковаться разные мысли по проекту, вызывающие трудности. Также могут быть уведомления о разных изменениях, например, если станет понятно, что кубер ставить еще рано, как уже бывало 🙂 И, разумеется, весь код публикуется на гитлабе. Код открыт, потому что наша задача — это создать облако как продукт и для каждого.
ссылка на оригинал статьи https://habr.com/ru/post/663868/