В своей предыдущей статье я уже упоминал, что создаю своё микрооблако. Точнее, ну как своё, это будет облако как продукт, а не как услуга. Поэтому оно такое же моё, как и ваше. Как и всего мира. То есть каждый сможет его развернуть на своё железо, будь то датацентр или пара-тройка виртуальных машин и пользоваться им в своё удовольствие, запуская на нем свои потрясающие проекты. Можно даже найти какой-то готовый проект, который лежит где-нибудь на гитхабе и давно просится на старт, только автору исходного кода запускать его было негде, потому что хранение исходников у нас бесплатное, а вот поддержка работы своего сервиса к дешевому сегменту экономики на мой взгляд не относится. А я такой запустил облако, вкинул туда чужой проект и помог миру увидеть то, что так и могло остаться в глубинах гитхаба. Но это фантазии, до реализации которых еще далеко, но путешествие в тысячу миль начинается с первого шага, главное не прекращать перебирать ногами. Поэтому мы продолжаем...
Так вот в прошлом посте в рамках проекта 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
;
$TTL 604800
@ IN SOA localhost. 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
;
$TTL 604800
@ IN SOA localhost. 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, и что доступны имена всех наших машин. Для наших машин также проверяется и реверсивный днс-поиск.
Подводим итоги
Теперь у нас машины состоят в виртуальной сети и знают друг друга по именам.
Настало время устроить рок-н-ролл на этих машинах. На следующем этапе я планирую запустить на них кубер. Так шаг за шагом мы создадим что-то приличное в нашем микрооблаке.
И в завершении, как всегда, предлагаю присоединяться к нашему телеграмм каналу и чату. О всех крупных вехах развития проекта планируется создаваться публикации на хабре, но в канале будут публиковаться разные мысли по проекту, вызывающие трудности. Также могут быть уведомления о разных изменениях, например, если станет понятно, что кубер ставить еще рано, как уже бывало :) И, разумеется, весь код публикуется на гитлабе. Код открыт, потому что наша задача - это создать облако как продукт и для каждого.