Введение



В последнее время стремительно растет популярность Kubernetes — все больше и больше проектов внедряют его у себя. Я же хотел коснуться такого оркестратора, как Nomad: он отлично подойдет проектам, где уже используются другие решения от компании HashiCorp, например, Vault и Consul, а сами проекты не являются сложными в плане инфраструктуры. В данном материале будет инструкция по установке Nomad, объединения двух нод в кластер, а также интеграции Nomad с Gitlab.





Тестовый стенд



Немного о тестовом стенде: используются три виртуальных сервера с характеристиками 2 CPU, 4 RAM, 50 Gb SSD, объединенных в общую локальную сеть. Их названия и IP адреса:

  1. nomad-livelinux-01: 172.30.0.5
  2. nomad-livelinux-02: 172.30.0.10
  3. consul-livelinux-01: 172.30.0.15


Установка Nomad, Consul. Создание Nomad кластера



Приступим к базовой установке. Несмотря на простоту установки, я опишу ее для целостности статьи: по сути она была создана из черновиков и заметок для быстрого доступа в случае необходимости.

Прежде, чем приступить к практике обсудим теоретическую часть, потому что на данном этапе важно понимание будущей структуры.

У нас существует две ноды номада и мы хотим объединить их в кластер, также на будущее время нам потребуется автоматический скейлинг кластера — для этого нам понадобится Consul. С помощью этого инструмента кластеризация и добавление новых нод становится очень простой задачей: созданная нода Nomad подключается к Consul агенту, после чего производит подключение к существующему Nomad кластеру. Поэтому в начале мы установим Consul сервер, настроим базовую http авторизацию для веб-панели (она по умолчанию без авторизации и может быть доступна по внешнему адресу), а также сами Consul агенты на Nomad серверах, после чего только приступим к Nomad.

Установка инструментов компании HashiCorp очень проста: по сути, мы просто перемещаем бинарный файл в bin директорию, настраиваем конфигурационный файл инструмента и создаем его сервис файл.

Загружаем бинарный файл Consul и распаковываем его в домашнюю директорию пользователя:

root@consul-livelinux-01:~# wget https://releases.hashicorp.com/consul/1.5.0/consul_1.5.0_linux_amd64.zip
root@consul-livelinux-01:~# unzip consul_1.5.0_linux_amd64.zip
root@consul-livelinux-01:~# mv consul /usr/local/bin/


Теперь у нас есть готовый бинарный файл consul для дальнейшей настройки.

Для работы с Consul нам требуется создать уникальный ключ с помощью команды keygen:

root@consul-livelinux-01:~# consul keygen


Перейдем к настройке конфигурации Consul, создаем директорию /etc/consul.d/ со следующей структурой:

/etc/consul.d/
+-- bootstrap
¦   L-- config.json


В директории bootstrap будет находиться конфигурационный файл config.json — в нем мы зададим настройки Consul. Его содержимое:

{
"bootstrap": true,
"server": true,
"datacenter": "dc1",
"data_dir": "/var/consul",
"encrypt": "your-key",
"log_level": "INFO",
"enable_syslog": true,
"start_join": ["172.30.0.15"]
}


Разберем по отдельности основные директивы и их значения:

  • bootstrap: true. Включаем автоматическое добавление новых нод в случае их подключения. Отмечу, что мы не указываем здесь точное число ожидаемых нод.
  • server: true. Включаем режим сервера. Consul на этой виртуальной машине будет выступать на данный момент единственным сервером и мастером, ВМ Nomad’a буду клиентами.
  • datacenter: dc1. Указываем название датацентра для создания кластера. Он должен быть идентичен и на клиентах, и на серверах.
  • encrypt: your-key. Ключ, который также должен быть уникальным и совпадать на всех клиентах и серверах. Генерируется с помощью команды consul keygen.
  • start_join. В этом списке мы указываем список IP адресов, к которым будет осуществляться подключение. На данный момент оставляем только собственный адрес.


На данном этапе мы можем запустить consul с помощью командной строки:

root@consul-livelinux-01:~# /usr/local/bin/consul agent -config-dir /etc/consul.d/bootstrap -ui


Это неплохой способ отладки сейчас, однако, на постоянной основе использовать этот способ не получится по очевидным причинам. Создадим сервис файл для управления Consul через systemd:

root@consul-livelinux-01:~# nano /etc/systemd/system/consul.service


Содержимое файла consul.service:

[Unit]
Description=Consul Startup process
After=network.target
 
[Service]
Type=simple
ExecStart=/bin/bash -c '/usr/local/bin/consul agent -config-dir /etc/consul.d/bootstrap -ui' 
TimeoutStartSec=0
 
[Install]
WantedBy=default.target


Запускаем Consul через systemctl:

root@consul-livelinux-01:~# systemctl start consul


Проверяем: у нас сервис должен быть запущен, а выполнив команду consul members мы должны увидеть наш сервер:

root@consul-livelinux:/etc/consul.d# consul members
consul-livelinux    172.30.0.15:8301  alive   server  1.5.0  2         dc1  <all>


Следующий этап: установка Nginx и настройка проксирования, http авторизации. Устанавливаем nginx через пакетный менеджер и в директории /etc/nginx/sites-enabled создаем конфигурационный файл consul.conf со следующим содержимым:

upstream consul-auth {
    server localhost:8500;
}

server {

    server_name consul.doman.name;
    
    location / {
      proxy_pass http://consul-auth;
      proxy_set_header Host $host;
      auth_basic_user_file /etc/nginx/.htpasswd;
      auth_basic "Password-protected Area";
    }
}


Не забудьте создать .htpasswd файл и сгенерировать для него логин и пароль. Данный пункт требуется для того, чтобы веб-панель не была доступна всем, кто знает наш домен. Однако, при настройке Gitlab нам придется от этого отказаться — в противном случае мы не сможем задеплоить в Nomad наше приложение. В моем проекте и Gitlab, и Nomad находятся только в серой сети, поэтому такой проблемы здесь нет.

На остальных двух серверах устанавливаем Consul агенты по следующей инструкции. Повторяем действия с бинарным файлом:

root@nomad-livelinux-01:~# wget https://releases.hashicorp.com/consul/1.5.0/consul_1.5.0_linux_amd64.zip
root@nomad-livelinux-01:~# unzip consul_1.5.0_linux_amd64.zip
root@nomad-livelinux-01:~# mv consul /usr/local/bin/


По аналогии с предыдущим сервером создаем директорию для конфигурационных файлов /etc/consul.d со следующей структурой:

/etc/consul.d/
+-- client
¦   L-- config.json


Содержимое файла config.json:

{
    "datacenter": "dc1",
    "data_dir": "/opt/consul",
    "log_level": "DEBUG",
    "node_name": "nomad-livelinux-01",
    "server": false,
    "encrypt": "your-private-key",
    "domain": "livelinux",
    "addresses": {
      "dns": "127.0.0.1",
      "https": "0.0.0.0",
      "grpc": "127.0.0.1",
      "http": "127.0.0.1"
    },
    "bind_addr": "172.30.0.5", # локальный адрес вм
    "start_join": ["172.30.0.15"], # удаленный адрес консул сервера
    "ports": {
      "dns": 53
     }
}


Сохраняем изменения и переходим к настройке сервис-файла, его содержимое:

/etc/systemd/system/consul.service:

[Unit]
Description="HashiCorp Consul - A service mesh solution"
Documentation=https://www.consul.io/
Requires=network-online.target
After=network-online.target

[Service]
User=root
Group=root
ExecStart=/usr/local/bin/consul agent -config-dir=/etc/consul.d/client
ExecReload=/usr/local/bin/consul reload
KillMode=process
Restart=on-failure

[Install]
WantedBy=multi-user.target


Запускаем consul на сервере. Теперь, после запуска мы должны в nsul members увидеть настроенный сервис. Это будет означать, что он успешно подключился к кластеру как клиент. Повторите тоже самое на втором сервере и после этого мы сможем приступить к установке и настройке Nomad.

Более подробная установка Nomad описана в его официальной документации. Есть два традиционных способа установки: загрузка бинарного файла и компиляция из исходников. Я выберу первый способ.

Примечание: проект очень быстро развивается, часто выходят новые обновления. Возможно, к моменту завершения статьи выйдет новая версия. Поэтому рекомендую перед прочтением проверить актуальную версию Nomad на данный момент и загружать именно ее.

root@nomad-livelinux-01:~# wget https://releases.hashicorp.com/nomad/0.9.1/nomad_0.9.1_linux_amd64.zip
root@nomad-livelinux-01:~# unzip nomad_0.9.1_linux_amd64.zip
root@nomad-livelinux-01:~# mv nomad /usr/local/bin/
root@nomad-livelinux-01:~# nomad -autocomplete-install
root@nomad-livelinux-01:~# complete -C /usr/local/bin/nomad nomad
root@nomad-livelinux-01:~# mkdir /etc/nomad.d


После распаковки мы получим бинарный файл Nomad’a весом в 65 Мб — его необходимо переместить в /usr/local/bin.

Создадим data директорию для Nomad’a и отредактируем его service файл (его, скорее всего, не будет существовать в начале):

root@nomad-livelinux-01:~# mkdir --parents /opt/nomad
root@nomad-livelinux-01:~# nano /etc/systemd/system/nomad.service


Вставляем туда следующие строчки:

[Unit]
Description=Nomad
Documentation=https://nomadproject.io/docs/
Wants=network-online.target
After=network-online.target

[Service]
ExecReload=/bin/kill -HUP $MAINPID
ExecStart=/usr/local/bin/nomad agent -config /etc/nomad.d
KillMode=process
KillSignal=SIGINT
LimitNOFILE=infinity
LimitNPROC=infinity
Restart=on-failure
RestartSec=2
StartLimitBurst=3
StartLimitIntervalSec=10
TasksMax=infinity

[Install]
WantedBy=multi-user.target


Однако, не спешим запускать nomad — мы еще не создали его конфигурационный файл:

root@nomad-livelinux-01:~# mkdir --parents /etc/nomad.d
root@nomad-livelinux-01:~# chmod 700 /etc/nomad.d
root@nomad-livelinux-01:~# nano /etc/nomad.d/nomad.hcl
root@nomad-livelinux-01:~# nano /etc/nomad.d/server.hcl


Итоговая структура директории будет следующей:

/etc/nomad.d/
+-- nomad.hcl
L-- server.hcl


Файл nomad.hcl должен содержать следующую конфигурацию:

datacenter = "dc1"
data_dir = "/opt/nomad"


Содержимое файла server.hcl:

server {
  enabled = true
  bootstrap_expect = 1
}

consul {
  address             = "127.0.0.1:8500"
  server_service_name = "nomad"
  client_service_name = "nomad-client"
  auto_advertise      = true
  server_auto_join    = true
  client_auto_join    = true
}

bind_addr = "127.0.0.1" 

advertise {
  http = "172.30.0.5"
}

client {
  enabled = true
}


Не забудьте изменить конфигурационные файл на втором сервере — там потребуется поменять значение директивы http.

Последним на данном этапе остается настройка Nginx для проксирования и установки http авторизации. Содержимое файла nomad.conf:

upstream nomad-auth {
        server 172.30.0.5:4646;
}

server {

        server_name nomad.domain.name;
        
        location / {
	        proxy_pass http://nomad-auth;
	        proxy_set_header Host $host;
	        auth_basic_user_file /etc/nginx/.htpasswd;
		   auth_basic "Password-protected Area";
        }
        
}


Теперь мы можем получить доступ к веб-панели по внешней сети. Подключаемся и переходим на страницу servers:


Изображение 1. Список серверов в кластере Nomad

Оба сервера успешно отображаются в панели, тоже самое мы увидим в выводе команды nomad node status:


Изображение 2. Вывод команды nomad node status

Что же со стороны Consul? Давайте посмотрим. Переходим в панель управления Consul, на страницу nodes:

Изображение 3. Список нод в кластере Consul

Теперь у нас есть подготовленный Nomad, работающий в связке с Consul. В завершающем этапе мы приступим к самому интересному: настроим доставку Docker контейнеров из Gitlab в Nomad, а также поговорим о некоторых других его отличительных особенностях.

Создание Gitlab Runner



Для деплоя докер образов в Номад мы будем использовать отдельный раннер с бинарный файлом Nomad’а внутри (тут, кстати, можно отметить еще одну особенность приложений Hashicorp — по отдельности они представляют из себя единственный бинарный файл). Загрузите его в директорию раннера. Для него создадим простейший Dockerfile со следующим содержимым:


FROM alpine:3.9
RUN apk add --update --no-cache libc6-compat gettext
COPY nomad /usr/local/bin/nomad


В этом же проекте создаем .gitlab-ci.yml:

variables:
  DOCKER_IMAGE: nomad/nomad-deploy
  DOCKER_REGISTRY: registry.domain.name
 

stages:
  - build

build:
  stage: build
  image: ${DOCKER_REGISTRY}/nomad/alpine:3
  script:
    - tag=${DOCKER_REGISTRY}/${DOCKER_IMAGE}:latest
    - docker build --pull -t ${tag} -f Dockerfile .
    - docker push ${tag}


В итоге у нас будет доступный образ раннера Номада в Gitlab Registry, теперь мы можем перейти непосредственно в репозиторий проекта, создадим Pipeline и настроим nomad job Nomad’a.

Настройка проекта



Начнем с файла job’s для Nomad. Мой проект в этой статье будет достаточно примитивен: он будет состоять из одной таски. Содержимое .gitlab-ci будет следующим:

variables:
  NOMAD_ADDR: http://nomad.address.service:4646
  DOCKER_REGISTRY: registry.domain.name
  DOCKER_IMAGE: example/project

stages:
  - build
  - deploy

build:
  stage: build
  image: ${DOCKER_REGISTRY}/nomad-runner/alpine:3
  script:
    - tag=${DOCKER_REGISTRY}/${DOCKER_IMAGE}:${CI_COMMIT_SHORT_SHA}
    - docker build --pull -t ${tag} -f Dockerfile .
    - docker push ${tag}


deploy:
  stage: deploy
  image: registry.example.com/nomad/nomad-runner:latest
  script:
    - envsubst '${CI_COMMIT_SHORT_SHA}' < project.nomad > job.nomad
    - cat job.nomad
    - nomad validate job.nomad
    - nomad plan job.nomad || if [ $? -eq 255 ]; then exit 255; else echo "success"; fi
    - nomad run job.nomad
  environment:
    name: production
  allow_failure: false
  when: manual


Здесь деплой происходит в ручном режиме, но вы можете настроить его на изменение содержимого директории проекта. Pipeline же состоит из двух этапов: из сборки образа и его деплоя в номад. На первом этапе мы собираем докер образ и пушим его в наш Registry, на втором же запускаем нашу job в Nomad.

job "monitoring-status" {
    datacenters = ["dc1"]
    migrate {
        max_parallel = 3
        health_check = "checks"
        min_healthy_time = "15s"
        healthy_deadline = "5m"
    }

    group "zhadan.ltd" {
        count = 1
        update {
            max_parallel      = 1
            min_healthy_time  = "30s"
            healthy_deadline  = "5m"
            progress_deadline = "10m"
            auto_revert       = true
        }
        task "service-monitoring" {
            driver = "docker"

            config {
                image = "registry.domain.name/example/project:${CI_COMMIT_SHORT_SHA}"
                force_pull = true
                auth {
                    username = "gitlab_user"
                    password = "gitlab_password"
                }
                port_map {
                    http = 8000
                }
            }
            resources {
                network {
                    port "http" {}
                }
            }
        }
    }
}


Обратите внимание, что у меня закрытый Registry и для успешного пулла докер-образа мне требуется авторизоваться в нем. Лучшим решением в данном случае является заключение логина и пароля в Vault с последующей интеграцией его с Nomad. Nomad нативно поддерживает Vault. Но для начала в самом Vault установим необходимые policy для Nomad, их можно загрузить:

# Download the policy and token role
$ curl https://nomadproject.io/data/vault/nomad-server-policy.hcl -O -s -L
$ curl https://nomadproject.io/data/vault/nomad-cluster-role.json -O -s -L

# Write the policy to Vault
$ vault policy write nomad-server nomad-server-policy.hcl

# Create the token role with Vault
$ vault write /auth/token/roles/nomad-cluster @nomad-cluster-role.json


Теперь, создав необходимые policy мы в блоке task в файле job.nomad добавим интеграцию с Vault:

vault {
  enabled = true
  address = "https://vault.domain.name:8200"
  token = "token"
}


Я использую авторизацию по токену и прописываю его непосредственно здесь, также есть вариант указания токена как переменной при запуске nomad agent:

$ VAULT_TOKEN=<token> nomad agent -config /path/to/config


Теперь мы можем использовать ключи с Vault. Принцип работы прост: мы создаем файл в Nomad job, который будет хранить в себе значения переменных, например:

template {
                data = <<EOH
{{with secret "secrets/pipeline-keys"}}
REGISTRY_LOGIN="{{ .Data.REGISTRY_LOGIN }}"
REGISTRY_PASSWORD="{{ .Data.REGISTRY_LOGIN }}{{ end }}"

EOH
    destination = "secrets/service-name.env"
    env = true
}


Вот таким несложным подходом можно настроить доставку контейнеров в кластер Nomad и работать с ним в дальнейшем. Я же скажу, что в какой-то степени я симпатизирую Nomad — он больше подходит для небольших проектов, где Kubernetes может вызвать дополнительные сложности и не будет реализовывать свой потенциал до конца. К тому же, Nomad отлично подойдет начинающим — он просто устанавливается и конфигурируется. Однако, при тестировании на некоторых проектах я сталкиваюсь с проблемой его ранних версий — многих базовых функций просто нет или они некорректно работают. Тем не менее, я считаю, что Nomad будет дальше развиваться и в будущем обрастет нужными всем функциями.

Автор: Илья Андреев, под редакцией Алексея Жадан и команды «Лайв Линукс»

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


  1. t1gger
    24.05.2019 14:43

    Однако, при тестировании на некоторых проектах я сталкиваюсь с проблемой его ранней версий — многих базовых функций просто нет или они некорректно работают.

    полностью согласен. сейчас столкнулся именно с этим — некоторых базовых вещей до сих нет, хотя о них просили еще несколько лет назад. похоже, придется смотреть в сторону k8s, хотя изначально выбрал consul+nomad из-за своей простоты.

    И не могу не добавить, хотя статья и не про это: consul — это service discovery не только для нод nomad'а, но и для запускаемых им сервисов. Если в jobs файле добавить небольшую секцию service, а сам consul посадить слушать порт 53 и настроить рекурсоры, то можно всегда иметь возможность узнать не только IP ноды, где в данный момент хостится сервис, но и даже динамические порты, которые торчат наружу


    1. Propp
      24.05.2019 14:55

      А каких функций к примеру в Номаде в данный момент не хватает?


      1. t1gger
        24.05.2019 15:40

        лично я уткнулся в следующие проблемы:
        1) настройка частной сети в nomad полностью отсутствует, хотя в том же docker-compose это делается без проблем. приходится делать самим докером (docker network create <сеть>). Для решения этой проблемы советуют колхоз в виде дополнительного таска типа exec, который бы поднимал сеть, но этот подход упирается в проблему №2
        2) нет приоритетов тасков (по аналогии, в том же docker-compose есть depends_on), т.е. нельзя тот же таск, который создает сеть, поставить первым.
        3) хотелось бы обработки событий, типа onStart/onStop, чтобы иметь возможность таким образом решить все ту же проблему с сетью, потому что погасить ее ведь тоже как-то надо
        4) при попытке разобраться с инструкцией template напоролся на то, что нет ни офф документации по ней, ни каких-то отзывов пользователей. документация в общем и целом довольно скудная

        вот с такими проблемами пришлось столкнуться при попытке поковырять nomad и перенести туда группу приложений, которые прекрасно работают в docker-compose. Наверняка список не полон


        1. Propp
          24.05.2019 15:44

          Спасибо. К слову, проблемы 2-3 есть и в docker swarm mode, но он совсем уж примитивный и последний год не показывает особо признаков жизни.


  1. SyCraft Автор
    24.05.2019 14:59

    Да, в данном варианте мы не в полной степени использовали Consul — на проектах использую его также для service discovery других сервисов, healthcheck'ов и прочего. Хотел добавить информацию про это в статье, но тема достойна отдельной статьи.

    Из недостатка функций в Nomad столкнулся с проблемой организации кластера Redis — он подключается только через IP адрес и не поддерживает доменные имена (была идея сделать это через Consul). Nomad же не может дать ему внутренний IP адрес. Есть альтернативное решение, но оно показалось мне избыточным


  1. Fixdens
    27.05.2019 16:38

    Содержимое файла config.json: в конце фигурной скобки не хватает =)


    1. SyCraft Автор
      27.05.2019 16:38

      Все верно x)

      При копировании из файла пропустил эту скобку, исправил сейчас. Спасибо!


      1. Fixdens
        27.05.2019 17:09

        И есть сложные моменты, которые новичку сложно понять.
        при установке консул просят указать интерфейс ( так как делаем это все на ВМ)
        Необходимо добавить в конец:
        «bind_addr»: "{{ GetInterfaceIP \«ens18\» }}"

        А в конфиге server.hcl:
        bind_addr = «0.0.0.0»

        advertise {
        rpc = «IP-ADRESS:4647»
        serf= «IP-ADRESS:4648»
        }

        А в этом моменте, чуток подробней можно подробней?
        Что же со стороны Consul? Давайте посмотрим. Переходим в панель управления Consul, на страницу nodes:
        Куда переходить, какая машина, порт etc.