TL; DR
В этой статье мы будем разворачивать Docker-приложение, голосовалку на Swarm, Kubernetes и Nomad от Hashicorp. Я надеюсь, вы получите такое же удовольствие от чтения этой статьи, какое я получил, когда экспериментировал со всем этим.
Если вы работаете с технологиями, то быть любознательным необходимо. Это необходимо для того, чтобы постоянно обучаться и быть в курсе того, что происходит в сфере. Уж больно быстро все меняется.
Оркестрация контейнеров – настолько горячая тема для обсуждения, что даже, если у вас и есть любимый инструмент, все равно интересно посмотреть, как работают другие и узнать про них что-нибудь новое.
Приложение для голосования
Я использовал приложение для голосования в предыдущих статьях. Приложение работает на микросервисной архитектуре и состоит из 5 сервисов.
- Vote: фронтенд, позволяющий пользователю выбрать между собакой и кошкой
- Redis: база данных, где хранятся голоса
- Worker: сервис, который собирает голоса из Редиса и хранит результаты в базе данных Postgres
- Db: база данных Postgres, в которой хранятся результаты голосования
- Result: фронтенд показывает результаты голосования
Как мы видим в репозитории на github, в приложении есть несколько compose-файлов: https://github.com/dockersamples/example-voting-app
Docker-stack.yml – готовое для использования в продакшене представление приложения. Вот сам файл:
version: "3"
services:
redis:
image: redis:alpine
ports:
- "6379"
networks:
- frontend
deploy:
replicas: 1
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
db:
image: postgres:9.4
volumes:
- db-data:/var/lib/postgresql/data
networks:
- backend
deploy:
placement:
constraints: [node.role == manager]
vote:
image: dockersamples/examplevotingapp_vote:before
ports:
- 5000:80
networks:
- frontend
depends_on:
- redis
deploy:
replicas: 2
update_config:
parallelism: 2
restart_policy:
condition: on-failure
result:
image: dockersamples/examplevotingapp_result:before
ports:
- 5001:80
networks:
- backend
depends_on:
- db
deploy:
replicas: 1
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
worker:
image: dockersamples/examplevotingapp_worker
networks:
- frontend
- backend
deploy:
mode: replicated
replicas: 1
labels: [APP=VOTING]
restart_policy:
condition: on-failure
delay: 10s
max_attempts: 3
window: 120s
placement:
constraints: [node.role == manager]
visualizer:
image: dockersamples/visualizer:stable
ports:
- "8080:8080"
stop_grace_period: 1m30s
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
deploy:
placement:
constraints: [node.role == manager]
networks:
frontend:
backend:
volumes:
db-data:
Вообще, в этом файле обозначено 6 сервисов, а в архитектуре приложения – только 5. Дополнительный сервис — это visualizer, прекрасный инструмент, предоставляющий интерфейс, который показывает, где развернуты сервисы.
Docker Swarm
Docker Swarm – это инструмент для управления и создания кластеров Docker-контейнеров. С помощью Swarm администраторы и разработчики могут создавать и управлять кластером нод как единой виртуальной системой.
Компоненты Swarm
Кластер Swarm состоит из нескольких нод, некоторые работают как менеджеры, другие – как исполнители:
- Ноды-менеджеры отвечают за внутренне состояние кластера
- Ноды-исполнители выполняют задачи (= запускают контейнеры)
Менеджеры делят внутреннее распределенное хранилище, чтобы поддерживать согласованное состояние кластера. Это обеспечивается благодаря логам Raft.
В Swarm сервисы определяют, как должны быть развернуты приложения, и как они должны работать в контейнерах.
Установка Docker
Если у вас еще не установлен Docker, вы можете скачать Docker CE (Community Edition) для вашей ОC.
Создание Swarm
Как только Docker установлен, от работающего Swarm нас отделяет всего одна команда
$ docker swarm init
Это все, что требуется для кластера Swarm. Хоть это и кластер с одной нодой, но все же кластер со всеми сопутствующими процессами.
Развертывание приложения
Среди compose-файлов, доступных в репозитории приложения на github, нам нужен docker-stack.yml для того, чтобы развернуть приложение через Swarm.
$ docker stack deploy -c docker-stack.yml app
Creating network app_backend
Creating network app_default
Creating network app_frontend
Creating service app_visualizer
Creating service app_redis
Creating service app_db
Creating service app_vote
Creating service app_result
Creating service app_worker
Поскольку стек запущен на докере для мака, у меня есть доступ к приложению сразу c локальной машины. Можно выбрать кошек или собак с помощью интерфейса для голосования (порт 5000), а посмотреть результаты на порте 5001.
Я сейчас не буду вдаваться в детали, просто хотел показать, как легко можно развернуть приложение с помощью Swarm.
Если вам нужен более подробный разбор, как разворачивать приложение через Swarm с несколькими нодами, то можете прочитать эту статью.
Kubernetes
Kubernetes – платформа с открытым исходным кодом для автоматизации развертывания, масштабирования и управления контейнеризированными приложениями.
Концепция Kubernetes
Кластер Kubernetes состоит из одного или нескольких Мастеров и нод.
- Мастер отвечает за управление кластером (управление состоянием кластера, планирование задач, реагирование на событие в кластере и т.д.)
- Ноды (раньше их называли миньонами. Да-да, как в мультике «Гадкий я») обеспечивают рантайм для запуска контейнера приложения (через Pods)
Для ввода команд используется CLI kubectl. Ниже мы рассмотрим несколько примеров его использования.
Для того, чтобы понимать, как разворачиваются приложения, нужно знать о нескольких высокоуровневых объектах Kubernetes:
- Pod – это самая маленькая единица, которую можно развернуть на ноде. Это группа контейнеров, которые должны работать вместе. Но довольно часто Pod содержит всего один контейнер.
- ReplicaSet обеспечивает работу конкретного количества реплик пода.
- Deployment управляет ReplicaSet и позволяет производить rolling updates, синий/зеленый деплой, тестировать и т.д.
- Service определяет логический набор подов и политику получения доступа к ним
В этой части мы будем использовать Deployment и Service для каждого из сервисов приложения.
Установка kubectl
Kubectl – это командная строка для разворачивания и управления приложениями в Kubernetes
Для установки используем официальную документацию (https://kubernetes.io/docs/tasks/tools/install-kubectl/). Например, для установки на Мак нужно ввести следующие команды:
$ curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/darwin/amd64/kubectl
$ chmod +x ./kubectl
$ sudo mv ./kubectl /usr/local/bin/kubectl
Установка Minicube
Minicube – это всеобъемлющая настройка Kubenetes. Он создает локальные ВМ и запускает кластер нод, на котором работают все процессы Kubernetes. Бесспорно, это не тот инструмент, который стоит использовать для установки кластера продакшена, но его действительно удобно использовать для разработки и тестирования.
Как только Minicube установлен, нужна всего одна команда для установки кластера с одной нодой.
$ minikube start
Starting local Kubernetes v1.7.0 cluster…
Starting VM…
Downloading Minikube ISO
97.80 MB / 97.80 MB [==============================================] 100.00% 0s
Getting VM IP address…
Moving files into cluster…
Setting up certs…
Starting cluster components…
Connecting to cluster…
Setting up kubeconfig…
Kubectl is now configured to use the cluster.
Дескриптор Kubernetes
В Kubernetes контейнеры запускаются через ReplicaSet, которым управляет Deployment.
Ниже представлен пример .yml файла, описывающего Deployment. ReplicaSet обеспечивает запуск 2 реплик Nginx.
// nginx-deployment.yml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 2 # tells deployment to run 2 pods matching the template
template: # create pods using pod definition in this template
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
Для создания деплоймента необходимо использовать CLI kubectl.
Для создания приложения, состоящего из микросервисов, надо создавать файл деплоймента для каждого сервиса. Можно сделать это вручную, а можно с помощью Kompose.
Использование Kompose для создания деплойментов и сервисов
Kompose – это инструмент, конвертирующий compose-файлы Docker в файлы-дескрипторы используемые Kubernetes. С этим сервисом получается удобнее, и он ускоряет процесс миграции.
Примечание:
Kompose использовать необязательно, все можно написать вручную, но он существенно ускоряет процесс развертывания
- Kompose не учитывает все опции используемые в файле Docker Compose
- Kompose можно установить на Linux или Mac с помощью следующих команд:
# Linux
$ curl -L https://github.com/kubernetes/kompose/releases/download/v1.0.0/kompose-linux-amd64 -o kompose
# macOS
$ curl -L https://github.com/kubernetes/kompose/releases/download/v1.0.0/kompose-darwin-amd64 -o kompose
$ chmod +x kompose
$ sudo mv ./kompose /usr/local/bin/kompose
Перед тем, как запустить docker-stack.yml в Kompose, мы его немного изменим и удалим ключ деплоя каждого сервиса. Этот ключ не воспринимается, и из-за него могут возникнуть ошибки при генерировании файлов-дескрипторов. Можно еще удалить информацию о networks. В Kompose мы отдадим новый файл, который назовем docker-stack-k8s.yml.
version: "3"
services:
redis:
image: redis:alpine
ports:
- "6379"
db:
image: postgres:9.4
volumes:
- db-data:/var/lib/postgresql/data
vote:
image: dockersamples/examplevotingapp_vote:before
ports:
- 5000:80
depends_on:
- redis
result:
image: dockersamples/examplevotingapp_result:before
ports:
- 5001:80
depends_on:
- db
worker:
image: dockersamples/examplevotingapp_worker
visualizer:
image: dockersamples/visualizer:stable
ports:
- "8080:8080"
stop_grace_period: 1m30s
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
volumes:
db-data:
Из файла docker-stack-k8s.yml генерируем дескрипторы для приложения используя следующую команду:
$ kompose convert --file docker-stack-k8s.yml
WARN Volume mount on the host "/var/run/docker.sock" isn't supported - ignoring path on the host
INFO Kubernetes file "db-service.yaml" created
INFO Kubernetes file "redis-service.yaml" created
INFO Kubernetes file "result-service.yaml" created
INFO Kubernetes file "visualizer-service.yaml" created
INFO Kubernetes file "vote-service.yaml" created
INFO Kubernetes file "worker-service.yaml" created
INFO Kubernetes file "db-deployment.yaml" created
INFO Kubernetes file "db-data-persistentvolumeclaim.yaml" created
INFO Kubernetes file "redis-deployment.yaml" created
INFO Kubernetes file "result-deployment.yaml" created
INFO Kubernetes file "visualizer-deployment.yaml" created
INFO Kubernetes file "visualizer-claim0-persistentvolumeclaim.yaml" created
INFO Kubernetes file "vote-deployment.yaml" created
INFO Kubernetes file "worker-deployment.yaml" created
Мы видим, что для каждого сервиса создаются файл деплоймента и сервиса.
Мы получили только одно предупреждение. Связано оно с visualizer, потому что не может быть подключен сокет Докера. Мы не будем пытаться запустить этот сервис, а сфокусируемся на остальных.
Разворачивание приложения
Через kubectl создадим все компоненты указанные в файле-дескрипторе. Укажем, что файлы расположены в текущей папке.
$ kubectl create -f .
persistentvolumeclaim "db-data" created
deployment "db" created
service "db" created
deployment "redis" created
service "redis" created
deployment "result" created
service "result" created
persistentvolumeclaim "visualizer-claim0" created
deployment "visualizer" created
service "visualizer" created
deployment "vote" created
service "vote" created
deployment "worker" created
service "worker" created
unable to decode "docker-stack-k8s.yml":...
Примеч.: так как мы оставили измененный compose-файл в текущей папке, то получили ошибку, потому что нельзя его распарсить. Но эту ошибку можно без всякого риска проигнорировать.
С помощью этих команд можно посмотреть созданные Сервисы и Деплойменты.
Даем доступ к приложению из внешнего мира
Для получения доступа к интерфейсу vote и result нужно немного изменить созданные для них сервисы.
Вот сгенерированный дескриптор для vote:
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
io.kompose.service: vote
name: vote
spec:
ports:
- name: "5000"
port: 5000
targetPort: 80
selector:
io.kompose.service: vote
status:
loadBalancer: {}
Мы изменим тип сервиса и заменим ClusterIP на NodePort. ClusterIP делает сервис доступным внутри, а NodePort разрешает публикацию порта на каждой ноде кластера и делает его доступным всему миру. Сделаем тоже самое для result, потому что мы хотим и к vote, и к result был доступ извне.
apiVersion: v1
kind: Service
metadata:
labels:
io.kompose.service: vote
name: vote
spec:
type: NodePort
ports:
- name: "5000"
port: 5000
targetPort: 80
selector:
io.kompose.service: vote
Как только изменения внесены в оба сервиса (vote и result), можно их пересоздать.
$ kubectl delete svc vote
$ kubectl delete svc result
$ kubectl create -f vote-service.yaml
service "vote" created
$ kubectl create -f result-service.yaml
service "result" created
Доступ к приложению
Теперь получим детали сервисов vote и result и получим порты, которые они предоставляют.
vote доступен по порту 30069, а result – 31873. Теперь голосуем и видим результаты.
После того, как мы разобрались с базовыми компонентами Kubernetes, мы смогли легко развернуть приложение. И Kompose нам здорово помог.
Hashicorp’s Nomad
Nomad – это инструмент для управления кластером машин и запуска приложения на них. Он абстрагирует машины и точку размещения приложения и позволяет пользователям сказать, что они хотят запустить. А Nomad отвечает за то, где это будет запущено и как.
Концепция Nomad
Кластер Nomad состоит из агентов (agents), которые могут работать в режиме сервера (server) или клиента (client).
- Сервер отвечает за consensus protocol, который позволяет серверу выбирать лидера и осуществлять репликацию состояния.
- Клиенты очень легкие, потому что взаимодействуют с сервером, при этом сами почти ничего не делают. В нодах-клиентах выполняются tasks.
На кластере Nomad может быть запущено несколько видов задач.
Для того, чтобы развернуть приложение нужно понять основные концепции Nomad:
- Job – определяет, какие tasks должен выполнять Nomad. Это описано в job-файле (текстовый файл в формате hcl, Hashicorp Configuration Language). Job может содержать одну или несколько групп tasks.
- Group содержит несколько tasks, которые расположены на одной машине.
- Task – запущенный процесс, в нашем случае это Docker-контейнер
- Маппинг tasks в job выполняется с помощью Allocations. Allocation используется для того, чтобы tasks в job выполнялись на конкретной ноде.
Установка
В этом примере мы запустим приложение на Докер-хосте, созданном на с помощью Docker Machine. Локальный IP – 192.168.1.100. Для начала запустим Consul, который используется для обнаружения и регистрации сервисов. Мы запустим Nomad и развернем приложение как Job в Nomad.
Consul для регистрации и обнаружения сервисов
Для обнаружения и регистрации сервисов рекомендуется инструмент, например, Consul, который не будет работать как Job в Nomad. Consul можно скачать по ссылке.
Эта команда запускает локально сервер Consul:
$ consul agent -dev -client=0.0.0.0 -dns-port=53 -recursor=8.8.8.8
Давайте поподробнее разберем используемы опции:
- -dev – флаг, который устанавливает кластер Consul с сервером и клиентом. Эта опция должна использоваться только для разработки и тестирования.
- -client=0.0.0.0 позволяет достичь сервисы Consul (API и DNS-сервер) через любой интерфейс хоста. Это необходимо, так как Nomad будет присоединяться к Consul через интерфейс localhost, а контейнеры – через docker-bridge (что-то вроде 172.17.х.х).
- -dns-port=53 определяет порт, который будет использовать DNS-сервер Consul (по умолчанию 8600). Мы установим стандартный 53 порт, чтобы DNS’ом можно было пользоваться из контейнера.
- -recursor=8.8.8.8 определяет другой DNS-сервер, который будет обрабатывать запросы, с которыми не может справиться Consul
Nomad можно скачать по этой ссылке.
Создаем кластер с нодой
Мы скачали Nomad и теперь можем запустить Агента (agent) со следующими настройками.
// nomad.hcl
bind_addr = "0.0.0.0"
data_dir = "/var/lib/nomad"
server {
enabled = true
bootstrap_expect = 1
}
client {
enabled = true
network_speed = 100
}
Агент будет работать и как сервер, и как клиент. Укажем, что bind_addr должен работать с любым интерфейсом, чтобы tasks можно было принимать и из внешнего мира. Запустим Агента Nomad со следующими настройками:
$ nomad agent -config=nomad.hcl
==> WARNING: Bootstrap mode enabled! Potentially unsafe operation.
Loaded configuration from nomad-v2.hcl
==> Starting Nomad agent...
==> Nomad agent configuration:
Client: true
Log Level: INFO
Region: global (DC: dc1)
Server: true
Version: 0.6.0
==> Nomad agent started! Log data will stream in below:
Примечание: по умолчанию Nomad подключается к локальному инстансу Consul.
Мы только что установили кластер с одной нодой. Вот информация по уникальному участнику:
$ nomad server-members
Name Address Port Status Leader Protocol Build Datacenter Region
neptune.local.global 192.168.1.100 4648 alive true 2 0.6.0 dc1 global
Разворачивание приложения
Чтобы развернуть приложение с помощью Swarm, можно использовать сразу compose-файл. Чтобы развернуть через Kubernetes – нужны дескрипторы из тех же compose-файлов. Как же все это происходит через Nomad?
Во-первых, нет инструмента похожего на Kompose для Hashicorp, чтобы он мог упростить миграцию compose на Nomad (неплохая идея для OpenSource-проекта, кстати). Файлы, описывающие Jobs, groups, tasks, надо писать вручную.
Мы разберем это подробнее, когда будем описывать Jobs для сервисов Redis и Vote. Для остальных сервисов это будет выглядеть примерно также.
Определяем Job для Redis
Этот файл определяет часть Redis в приложении:
// redis.nomad
job "redis-nomad" {
datacenters = ["dc1"]
type = "service"
group "redis-group" {
task "redis" {
driver = "docker"
config {
image = "redis:3.2"
port_map {
db = 6379
}
}
resources {
cpu = 500 # 500 MHz
memory = 256 # 256MB
network {
mbits = 10
port "db" {}
}
}
service {
name = "redis"
address_mode = "driver"
port = "db"
check {
name = "alive"
type = "tcp"
interval = "10s"
timeout = "2s"
}
}
}
}
}
Давайте разберем, что же тут написано:
- Имя Job – redis-nomad
- Тип Job – сервис (т.е. длительная операция)
- Группе дано произвольное название; содержит одну операцию
- Task Redis использует docker-driver, то есть он будет в запущен в контейнере
- Task будет использовать образ Redis:3.2
- В блоке с ресурсами указаны ограничения для CPU и memory
- В блоке network указано, что порт db должен быть динамическим
- В блоке service определено, как будет проходить регистрация в Consul: имя сервиса, IP-адрес и определение проверки здоровья
Для того, чтобы проверить, будет ли Job выполняться правильно, используем команду plan:
$ nomad plan redis.nomad
+ Job: "nomad-redis"
+ Task Group: "cache" (1 create)
+ Task: "redis" (forces create)
Scheduler dry-run:
- All tasks successfully allocated.
Job Modify Index: 0
To submit the job with version verification run:
nomad run -check-index 0 redis.nomad
When running the job with the check-index flag, the job will only be run if the server side version matches the job modify index returned. If the index has changed, another user has modified the job and the plan's results are potentially invalid.
Кажется, все работает. Теперь развернем task с этим job:
$ nomad run redis.nomad
==> Monitoring evaluation "1e729627"
Evaluation triggered by job "nomad-redis"
Allocation "bf3fc4b2" created: node "b0d927cd", group "cache"
Evaluation status changed: "pending" -> "complete"
==> Evaluation "1e729627" finished with status "complete"
Видим, что размещение создано. Проверим его статус:
$ nomad alloc-status bf3fc4b2
ID = bf3fc4b2
Eval ID = 1e729627
Name = nomad-redis.cache[0]
Node ID = b0d927cd
Job ID = nomad-redis
Job Version = 0
Client Status = running
Client Description = <none>
Desired Status = run
Desired Description = <none>
Created At = 08/23/17 21:52:03 CEST
Task "redis" is "running"
Task Resources
CPU Memory Disk IOPS Addresses
1/500 MHz 6.3 MiB/256 MiB 300 MiB 0 db: 192.168.1.100:21886
Task Events:
Started At = 08/23/17 19:52:03 UTC
Finished At = N/A
Total Restarts = 0
Last Restart = N/A
Recent Events:
Time Type Description
08/23/17 21:52:03 CEST Started Task started by client
08/23/17 21:52:03 CEST Task Setup Building Task Directory
08/23/17 21:52:03 CEST Received Task received by client
Контейнер запущен корректно. Проверим DNS-сервер Consul и убедимся, что сервис также корректно регистрируется:
$ dig @localhost SRV redis.service.consul
; <<>> DiG 9.10.3-P4-Ubuntu <<>> @localhost SRV redis.service.consul
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35884
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 2
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;redis.service.consul. IN SRV
;; ANSWER SECTION:
redis.service.consul. 0 IN SRV 1 1 6379 ac110002.addr.dc1.consul.
;; ADDITIONAL SECTION:
ac110002.addr.dc1.consul. 0 IN A 172.17.0.2
;; Query time: 0 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Wed Aug 23 23:08:36 CEST 2017
;; MSG SIZE rcvd: 103
Task был размещен по IP 172.17.0.2, а его порт- 6379, как мы и указывали.
Определяем Job для Vote
Определим Job для сервиса vote. Используем следующий файл:
// job.nomad
job "vote-nomad" {
datacenters = ["dc1"]
type = "service"
group "vote-group" {
task "vote" {
driver = "docker"
config {
image = "dockersamples/examplevotingapp_vote:before"
dns_search_domains = ["service.dc1.consul"]
dns_servers = ["172.17.0.1", "8.8.8.8"]
port_map {
http = 80
}
}
service {
name = "vote"
port = "http"
check {
name = "vote interface running on 80"
interval = "10s"
timeout = "5s"
type = "http"
protocol = "http"
path = "/"
}
}
resources {
cpu = 500 # 500 MHz
memory = 256 # 256MB
network {
port "http" {
static = 5000
}
}
}
}
}
}
Но есть тут несколько отличий от того файла, который мы использовали для Redis:
- Vote подключается к redis используя только имя операции. Вот пример части файла app.py, используемого в сервисе vote:
// app.py
def get_redis():
if not hasattr(g, 'redis'):
g.redis = Redis(host="redis", db=0, socket_timeout=5)
return g.redis
В этом случае для получения IP контейнера с redis контейнер с vote должен использовать DNS-сервер Consul. DNS запрос из контейнера выполняется через Docker bridge (172.17.0.1). dns_search_domains определяет, что Сервис Х зарегистрирован как X.service.dc1.consul внутри Consul
- Мы установили статичный порт, чтобы сервис vote на 5000 порте был доступен извне кластера.
Мы можем сделать такую же настройку и для остальных сервисов: worker, postgres и result.
Доступ к приложению
Когда все Jobs запущены, можно проверить их статус и убедиться, что все работает.
Мы также можем посмотреть это через интерфейс Consul.
По IP ноды (в нашем случае 192.168.1.100) получаем доступ к интерфейсам с vote и result.
Итог
Вот такая голосовалка прекрасное приложение с точки зрения демонстрации. Мне было интересно узнать, может ли оно быть развернуто без изменений в коде с помощью какого-нибудь оркестратора. И да, можно, даже без каких-то особых танцев с бубном.
Я надеюсь, что эта статья вам поможет в понимании основ Swarm, Kubernetes и Nomad. Также было бы интересно узнать, что вы запускаете в Docker и каким пользуетесь оркестратором.
Автор: helpik94