В своей предыдущей статье я уже упоминал, что создаю своё микрооблако. Точнее, ну как своё, это будет облако как продукт, а не как услуга. Поэтому оно такое же моё, как и ваше. Как и всего мира. То есть каждый сможет его развернуть на своё железо, будь то датацентр или пара-тройка виртуальных машин и пользоваться им в своё удовольствие, запуская на нем свои потрясающие проекты. Можно даже найти какой-то готовый проект, который лежит где-нибудь на гитхабе и давно просится на старт, только автору исходного кода запускать его было негде, потому что хранение исходников у нас бесплатное, а вот поддержка работы своего сервиса к дешевому сегменту экономики на мой взгляд не относится. А я такой запустил облако, вкинул туда чужой проект и помог миру увидеть то, что так и могло остаться в глубинах гитхаба. Но это фантазии, до реализации которых еще далеко, но путешествие в тысячу миль начинается с первого шага, главное не прекращать перебирать ногами. Поэтому мы продолжаем...

Так вот в прошлом посте в рамках проекта 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, и что доступны имена всех наших машин. Для наших машин также проверяется и реверсивный днс-поиск.

Подводим итоги

Теперь у нас машины состоят в виртуальной сети и знают друг друга по именам.

Настало время устроить рок-н-ролл на этих машинах. На следующем этапе я планирую запустить на них кубер. Так шаг за шагом мы создадим что-то приличное в нашем микрооблаке.

И в завершении, как всегда, предлагаю присоединяться к нашему телеграмм каналу и чату. О всех крупных вехах развития проекта планируется создаваться публикации на хабре, но в канале будут публиковаться разные мысли по проекту, вызывающие трудности. Также могут быть уведомления о разных изменениях, например, если станет понятно, что кубер ставить еще рано, как уже бывало :) И, разумеется, весь код публикуется на гитлабе. Код открыт, потому что наша задача - это создать облако как продукт и для каждого.

Комментарии (0)