OpenStack: модуль оркестрации Heat

Рассмотрим построение готовой инфраструктуры из виртуальных устройств при помощи модуля оркестрации Heat.

Для работы с Heat потребуется пакет python-heat, который присутствует в репозиториях большинства современных дистрибутивов. Установить его можно из репозитория PyPI c помощью утилиты PIP. Все инструкции по установке можно найти на вкладке Доступ в подразделе Проекты раздела Виртуальное приватное облако панели управления.

Основные понятия

Стек (англ. stack) — это набор облачных ресурсов (машин, логических томов, сетей и т.д.), объединённых в цельную структуру.

Шаблон — это описание стека. Обычно оно представлено в виде текстового файла в особом формате. Шаблон содержит описание ресурсов и их связи. При этом ресурсы могут быть описаны в любом порядке: сборка стека осуществляется в автоматическом режиме. Созданные ранее стеки можно использовать в качестве ресурса для описания в других шаблонах, что позволяет создавать так называемые вложенные стеки (nested stacks).

Структуру шаблонов и правила их написания рассмотрим на практическом примере. Создадим стек, состоящий из двух серверов, локальной сети и роутера, через который будет осуществляться выход в открытую сеть.

Форматы шаблонов

Шаблоны могут быть представлены в нескольких форматах. К использованию рекомендуется формат HOT, который был создан специально для проекта Heat и отличается достаточно простым и понятым синтаксисом. Формат основан на YAML, поэтому при редактировании текста важно следить за использованием пробелов в отступах и их иерархии.

Для обеспечения совместимости с шаблонами, используемыми в Amazon EC2, поддерживается также формат CFN (AWS CloudFormation).

Структура шаблона

Создается стек при помощи следующего шаблона:

heat_template_version: 2013-05-23

description: Basic template of two servers, one network and one router

parameters:
  key_name:
    type: string
    description: Name of keypair to assign to servers for ssh authentication
  public_net_id:
    type: string
    description: UUID of public network to outer world
  server_flavor:
    type: string
    description: UUID of virtual hardware configurations that are called flavors in openstack
  private_net_name:
    type: string
    description: Name of private network (L2 level)
  private_subnet_name:
    type: string
    description: Name of private network subnet (L3 level)
  router_name:
    type: string
    description: Name of router that connects private and public networks
  server1_name:
    type: string
    description: Custom name of server1 virtual machine
  server2_name:
    type: string
    description: Custom name of server2 virtual machine
  image_centos7:
    type: string
    description: UUID of glance image with centos 7 distro
  image_debian7:
    type: string
    description: UUID of glance image with debian 7 distro

resources:
  
  private_net:
    type: OS::Neutron::Net
    properties:
      name: { get_param: private_net_name }

  private_subnet:
    type: OS::Neutron::Subnet
    properties:
      name: { get_param: private_subnet_name }
      network_id: { get_resource: private_net }
      allocation_pools:
        - start: "192.168.0.10"
          end: "192.168.0.254"
      cidr: "192.168.0.0/24"
      enable_dhcp: True
      gateway_ip: "192.168.0.1"

  router:
    type: OS::Neutron::Router
    properties:
      name: { get_param: router_name }
      external_gateway_info: { "enable_snat": True, "network": { get_param: public_net_id }}

  router_interface:
    type: OS::Neutron::RouterInterface
    properties:
      router_id: { get_resource: router }
      subnet_id: { get_resource: private_subnet }

  server1:
    type: OS::Nova::Server
    properties:
      name: { get_param: server1_name }
      block_device_mapping:
        - volume_size: 5
          volume_id: { get_resource: "server1_disk" }
          device_name: "/dev/vda"
      config_drive: "False"
      flavor: { get_param: server_flavor }
      image: { get_param: image_centos7 }
      key_name: { get_param: key_name }
      networks:
        - port: { get_resource: server1_port }

  server1_disk:
    type: OS::Cinder::Volume
    properties:
      name: server1_disk
      image: { get_param: image_centos7 }
      size: 5

  server1_port:
    type: OS::Neutron::Port
    properties:
      network_id: { get_resource: private_net }
      fixed_ips:
        - subnet_id: { get_resource: private_subnet }

  server1_floating_ip:
    type: OS::Neutron::FloatingIP
    properties:
      floating_network_id: { get_param: public_net_id }
      port_id: { get_resource: server1_port }
    depends_on: router_interface

  server2:
    type: OS::Nova::Server
    properties:
      name: { get_param: server2_name }
      block_device_mapping:
        - volume_size: 5
          volume_id: { get_resource: "server2_disk" }
          device_name: "/dev/vda"
      config_drive: "False"
      flavor: { get_param: server_flavor }
      image: { get_param: image_debian7 }
      key_name: { get_param: key_name }
      networks:
        - port: { get_resource: server2_port }

  server2_disk:
    type: OS::Cinder::Volume
    properties:
      name: server2_disk
      image: { get_param: image_debian7 }
      size: 5

  server2_port:
    type: OS::Neutron::Port
    properties:
      network_id: { get_resource: private_net }
      fixed_ips:
        - subnet_id: { get_resource: private_subnet }
        
outputs:
  server1_private_ip:
    description: private ip within local subnet of server1 with installed Centos 7 distro
    value: { get_attr: [ server1_port, fixed_ips, 0, ip_address ] }
  server1_public_ip:
    description: floating_ip that is assigned to server1 server
    value: { get_attr: [ server1_floating_ip, floating_ip_address ] }
  server2_private_ip:
    description: private ip within local subnet of server2 with installed Debian 7 distro
    value: { get_attr: [ server2, first_address ] }

Рассмотрим его структуру более подробно.

Шаблон состоит из нескольких блоков. В первом указывается версия шаблона и используемый формат описания. В каждом новом выпуске платформы openstack поддерживается свой набор свойств и атрибутов, который постепенно изменяется. В приводимых примерах используется версия 2013-05-23, которая поддерживает все свойства, реализованные при выпуске релиза Icehouse.

heat_template_version: 2013-05-23

           description: >
  Basic template of two servers, one network and one router

Во втором блоке приводится общее описание шаблона и его назначения:

parameters:
  key_name:
    type: string
    description: Name of keypair to assign to servers for ssh authentication
  public_net_id:
    type: string
    description: UUID of public network to outer world
    default: 98863f6c-638e-4b48-a377-01f0e86f34ae
  server_flavor:
    type: string
    description: UUID of virtual hardware configurations that are called flavors in openstack
  private_net_name:
    type: string
    description: The Name of private network (L2 level)
  private_subnet_name:
    type: string
    description: the Name of private subnet (L3 level)
  router_name:
    type: string
    description: The Name of router that connects private and public networks
  server1_name:
    type: string
    description: Custom name of server1 virtual machine
  server2_name:
    type: string
    description: Custom name of server2 virtual machine
  image_centos7:
    type: string
    description: UUID of glance image with centos 7 distro
  image_debian7:
    type: string
    description: UUID of glance image with debian 7 distro

Далее перечисляются некоторые дополнительные параметры, которые будут переданы Heat при создании стека. В параметре key_name указывается пара ключей для подключения к созданному серверу по ssh. А в параметрах server_flavor и public_net_id — идентификаторы (UUID) «аппаратной» конфигурации виртуальной машины и публичной сети.

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

resources:

  private_net:
    type: OS::Neutron::Net
    properties:
      name:  { get_param: private_net_name }

  private_subnet:
    type: OS::Neutron::Subnet
    properties:
      name: { get_param: private_subnet_name }
      network_id: { get_resource: private_net }
      allocation_pools:
        - start: "192.168.0.10"
          end: "192.168.0.254"
      cidr: "192.168.0.0/24"
      enable_dhcp: True
      gateway_ip: "192.168.0.1"

  router:
    type: OS::Neutron::Router
    properties:
      name: { get_param: router_name }
      external_gateway_info: { "enable_snat": True, "network": { get_param: public_net_id}}

  router_interface:
    type: OS::Neutron::RouterInterface
    properties:
      router_id: { get_resource: router }
      subnet_id: { get_resource: private_subnet }

  server1:
    type: OS::Nova::Server
    properties:
      name: { get_param: server1_name }
      block_device_mapping:
        - volume_size: 5
          volume_id: { get_resource: "server1_disk" }
          device_name: "/dev/vda"
      config_drive: "False"
      flavor: { get_param: server_flavor }
      image: { get_param: image_server1 }
      key_name: { get_param: key_name }
      networks:
        - port: { get_resource: server1_port }

  server1_disk:
    type: OS::Cinder::Volume
    properties:
      name: server1_disk
      image: { get_param: image_server1 }
      size: 5

  server1_port:
    type: OS::Neutron::Port
    properties:
      network_id: { get_resource: private_net }
      fixed_ips:
        - subnet_id: { get_resource: private_subnet }

  server1_floating_ip:
    type: OS::Neutron::FloatingIP
    properties:
      floating_network_id: { get_param: public_net_id }
      port_id: { get_resource: server1_port }
    depends_on: router_interface

  server2:
    type: OS::Nova::Server
    properties:
      name: { get_param: server2_name }
      block_device_mapping:
        - volume_size: 5
          volume_id: { get_resource: "server2_disk" }
          device_name: "/dev/vda"
      config_drive: "False"
      flavor: { get_param: server_flavor }
      image: { get_param: image_server2 }
      key_name: { get_param: key_name }
      networks:
        - port: { get_resource: server2_port }

  server2_disk:
    type: OS::Cinder::Volume
    properties:
      name: server2_disk
      image: { get_param: image_server2 }
      size: 5

  server2_port:
    type: OS::Neutron::Port
    properties:
      network_id: { get_resource: private_net }
      fixed_ips:
        - subnet_id: { get_resource: private_subnet }

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

Следующий этап — создание роутера и интерфейса на нём. Через этот интерфейс роутер подключается к созданной локальной сети. Затем перечисляются серверы. У каждого сервера должно быть по порту и диску. Для первого сервера, в отличие от второго, указан также плавающий IP-адрес (floating_ip), с помощью которого внешний адрес из публичной сети можно ассоциировать с «серым» адресом из локальной.

 server1_floating_ip:
    type: OS::Neutron::FloatingIP
    properties:
      floating_network_id: { get_param: public_net_id }
      port_id: { get_resource: server1_port }
    depends_on: router_interface

Обратите внимание на то, как используются параметры и ресурсы при описании новых устройств. Выше приведен фрагмент описания ресурса плавающего IP-адреса для первого сервера. В его свойствах необходимо указать UUID публичной сети, откуда будет взят плавающий IP-адрес (floating_network_id) и UUID порта сервера (port_id), с которым этот адрес будет связан. В функции get_param указывается, что значение следует брать из параметра public_net_id (ниже будет описано, как использовать параметры). Идентификатора порта первого сервера ещё нет. Он появится только после того, как сервер будет создан.

Функция get_resource указывает, что сразу после создания ресурса server1_port его значение должно использоваться в качестве UUID для port_id.

Resource DELETE failed: Conflict: Router interface for subnet 8958ffad-7622-4d98-9fd9-6f4423937b59 on router 7ee9754b-beba-4301-9bdd-166117c5e5a6 cannot be deleted, as it is required by one or more floating IPs.

Согласно этому сообщению, роутер не может быть удалён, потому что к сети, ассоциированной с этим роутером, привязаны плавающие IP-адреса. Вполне ожидаемо, что при удалении стека необходимо в первую очередь удалить плавающие IP-адреса, а уже затем роутер и связанную с ним сеть. Проблема заключается в том, что все ресурсы компонентов neutron, cinder, nova, glance являются независимыми друг от друга сущностями, между которыми устанавливаются связи и отношения.

В большинстве случаев Heat определяет нужный порядок создания ресурсов и построения между ними связей при создании стека. При удалении стека эти связи также будут учитываться: по ним будет определён порядок удаления ресурсов. Но иногда, как в приведённом выше примере, возникают ошибки. С помощью директивы depends_on явно указано, что плавающий IP-адрес связан с роутером и интерфейсом на нём. Благодаря этой связи, теперь IP-адрес будет создаваться после того, как будет создан роутер и интерфейс на нём. При удалении всё будет происходить в обратном порядке: сначала будет удалён плавающий IP-адрес, а затем — роутер и его интерфейс.

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

outputs:
  server1_private_ip:
    description: private ip address within local subnet  of server 1 with installed Centos7 distro
    value: { get_attr: [ server1_port, fixed_ips, 0, ip_address]}
  server1_public_ip:
    description: floating ip that is assigned to server1 server
    value: { get_attr: [ server1_floating_ip, floating_ip_address ]}
  server2_private_ip:
    description: private ip address within local subnet of server2 with installed Debian7 distro
    value: { get_attr: [ server2, first_address ]}

В приведённом фрагменте указывается, что желательно получить следующие значения для создаваемых в процессе сборки стека ресурсов: адрес первого сервера в локальной сети, публичный адрес первого сервера (плавающий IP-адрес) и адрес второго сервера в локальной сети. Для каждого параметра указано его краткое описание и запрашиваемое значение (value). Для этого используется функция get_attr, которой необходимо два значения: первый — имя ресурса, второй — его атрибуты.

Обратите внимание на разные способы получения адреса в локальной сети у первого и второго серверов. Оба варианта допустимы и равнозначны. Разница в том, что в первом случае происходит обращение к компоненту Neutron (у server1_port тип «OS::Neutron::Port») и берётся первый IP-адрес из атрибута fixed_ips. Во втором случае часто упоминаемый в примерах шаблонов в сети, происходит обращение к компоненту nova (ресурс server2 с типом «OS::Nova::Server») и атрибуту first_address.

Такие компоненты платформы Openstack, как Neutron и Cinder, появились позже, чем Nova. Поэтому Nova раньше использовался для гораздо большего количества функций, в том числе и для управления дисками и сетями. С полноценным развитием Neutron и Cinder такая необходимость отпала, но оставлена в целях совместимости. Политика в отношении Nova постепенно пересматривается, и некоторые функции со временем объявляются устаревшими. Возможно, что и атрибут first_address скоро не будет поддерживаться.

value: { get_attr: [ server1_port, fixed_ips, 0, ip_address]}
value: { get_attr: [ server2, first_address ]}

Более подробно о шаблонах и правилах их составления можно прочитать в официальном руководстве.

Создание стека

Подготовив шаблон, проверим его на наличие синтаксических ошибок и на соответствие стандарту: 

$ heat template-validate -f publication.yml 

Если шаблон составлен правильно, то в качестве ответа будет представлен вывод в формате json:

{
  "Description": "Basic template of two servers, one network and one router\n", 
  "Parameters": {
    "server2_name": {
      "NoEcho": "false", 
      "Type": "String", 
      "Description": "", 
      "Label": "server2_name"
    }, 
    "private_subnet_name": {
      "NoEcho": "false", 
      "Type": "String", 
      "Description": "the Name of private subnet", 
      "Label": "private_subnet_name"
    }, 
    "key_name": {
      "NoEcho": "false", 
...

Для создания стека необходимо следующее:

$ heat stack-create TESTA -f testa.yml -P key_name="testa" \
-P public_net_id="ab2264dd-bde8-4a97-b0da-5fea63191019" \
-P server_flavor="1406718579611-8007733592" \
-P private_net_name=localnet -P private_subnet_name="192.168.0.0/24" \
-P router_name=router -P server1_name=Centos7 -P server2_name=Debian7 \
-P image_server1="CentOS 7 64-bit" \
-P image_server2="ba78ce9b-f800-4fb2-ad85-a68ca0f19cb8"

Каждый раз передавать параметры клиенту Heat вручную неудобно: легко можно сделать ошибку. Чтобы избежать этого недостатка, создайте дополнительный файл, повторяющий формат основного шаблона, но содержащий только самые основные параметры:

parameters:
  key_name:  testa
  public_net_id: ab2264dd-bde8-4a97-b0da-5fea63191019
  server_flavor: myflavor
  private_net_name: localnet
  private_subnet_name: 192.168.0.0/24
  router_name: router
  server1_name: Centos7
  server2_name: Debian7
  image_server1: CentOS 7 64-bit
  image_server2: ba78ce9b-f800-4fb2-ad85-a68ca0f19cb8

В этом случае создание стека с помощью консольной утилиты Heat будет упрощено:

$ heat stack-create TESTA -f testa.yml -e testa_env.yml
+--------------------------------------+------------+--------------------+----------------------+
| id                                   | stack_name | stack_status       | creation_time        |
+--------------------------------------+------------+--------------------+----------------------+
| 96d37fd2-52e8-4b59-bf42-2ce72566e03e | TESTA      | CREATE_IN_PROGRESS | 2014-12-17T15:17:17Z |
+--------------------------------------+------------+--------------------+----------------------+

Чтобы узнать необходимые значения передаваемых Heat параметров, можно использовать стандартный набор утилит для работы с Openstack. Например, узнать идентификатор публичной сети public_net_id можно с использованием Neutron:

$ neutron net-list
+--------------------------------------+------------------+-----------------------------------------------------+
| id                                   | name             | subnets                                             |
+--------------------------------------+------------------+-----------------------------------------------------+
| 168bb122-a00a-4e34-bcc9-3bd0b417ee2b | localnet         | 256647b7-7b73-4534-8a79-1901c9b25527 192.168.0.0/24 |
| ab2264dd-bde8-4a97-b0da-5fea63191019 | external-network | 102a9263-2d84-4335-acfb-6583ac8e70aa                |
|                                      |                  | aa9e4fc4-63b0-432e-bcbd-82a613310acb                |
+--------------------------------------+------------------+-----------------------------------------------------+

Чтобы узнать имя или идентификатор server_flavor и image_server1, image_server2 можно аналогичным образом воспользоваться соотвествующими утилитами.

Операции со стеком

После создания стека нужно убедиться в том, что всё ли прошло без ошибок, а также узнать, какие IP-адреса были присвоены серверам (прежде всего — публичный IP первого сервера).

Список всех созданных стеков можно получить с помощью команды heat-list. В её вывод будет включена информация о состоянии каждого стека:

$ heat stack-list
+--------------------------------------+------------+-----------------+----------------------+
| id                                   | stack_name | stack_status    | creation_time        |
+--------------------------------------+------------+-----------------+----------------------+
| e7ad8ef1-921d-4e70-a203-20dbc32d4a02 | TESTA      | CREATE_COMPLETE | 2014-12-17T18:30:54Z |
| ab5159d2-08ad-47a2-a964-a2c3425eca8f | TESTNODE   | CREATE_FAILED   | 2014-12-17T18:39:38Z |
+--------------------------------------+------------+-----------------+----------------------+

Как видно из вывода, UUID локальной сети, к которой должен быть подключен порт создаваемого сервера, указан неправильно — из-за этого и возникла ошибка. Также ошибки часто случаются из-за отсутствия свободных ресурсов (для каждого проекта выставляются лимиты количества используемых ядер, RAM и другие).

Если стек создан успешно, то в общем выводе команды stack-show появится также секция outputs, в которой содержатся интересующие значения:

+----------------------+----------------------------------------------------------------------------------------------------------------------------------+
| Property             | Value                                                                                                                            |
+----------------------+----------------------------------------------------------------------------------------------------------------------------------+
| capabilities         | []                                                                                                                               |
| creation_time        | 2014-12-17T15:17:17Z                                                                                                             |
| description          | Basic template of two servers, one network and one                                                                               |
|                      | router                                                                                                                           |
| disable_rollback     | True                                                                                                                             |
| id                   | 96d37fd2-52e8-4b59-bf42-2ce72566e03e                                                                                             |
| links                | https://api.selvpc.ru/orchestration/v1/58ad5a5408ad4ad5864f260308884539/stacks/TESTA/96d37fd2-52e8-4b59-bf42-2ce72566e03e (self) |
| notification_topics  | []                                                                                                                               |
| outputs              | [                                                                                                                                |
|                      |   {                                                                                                                              |
|                      |     "output_value": "192.168.0.10",                                                                                              |
|                      |     "description": "private ip within local subnet of server2 with installed Debian 7 distro",                                   |
|                      |     "output_key": "server2_private_ip"                                                                                           |
|                      |   },                                                                                                                             |
|                      |   {                                                                                                                              |
|                      |     "output_value": "192.168.0.13",                                                                                              |
|                      |     "description": "private ip within local subnet of server1 with installed Centos 7 distro",                                   |
|                      |     "output_key": "server1_private_ip"                                                                                           |
|                      |   },                                                                                                                             |
|                      |   {                                                                                                                              |
|                      |     "output_value": "95.213.154.134",                                                                                            |
|                      |     "description": "floating_ip that is assigned to server1 server",                                                             |
|                      |     "output_key": "server1_public_ip"                                                                                            |
|                      |   }                                                                                                                              |
|                      | ]                                                                                                                                |
| parameters           | {                                                                                                                                |
|                      |   "server2_name": "Debian7",                                                                                                     |
|                      |   "image_centos7": "CentOS 7 64-bit",                                                                                            |
|                      |   "OS::stack_id": "96d37fd2-52e8-4b59-bf42-2ce72566e03e",                                                                        |
|                      |   "OS::stack_name": "TESTA",                                                                                                     |
|                      |   "private_subnet_name": "192.168.0.0/24",                                                                                       |
|                      |   "key_name": "testa",                                                                                                           |
|                      |   "server1_name": "Centos7",                                                                                                     |
|                      |   "public_net_id": "ab2264dd-bde8-4a97-b0da-5fea63191019",                                                                       |
|                      |   "private_net_name": "localnet",                                                                                                |
|                      |   "router_name": "router",                                                                                                       |
|                      |   "server_flavor": "myflavor",                                                                                                   |
|                      |   "image_debian7": "d3e1be2a-e0fc-4cfc-ac07-35c9706f02cc"                                                                        |
|                      | }                                                                                                                                |
| stack_name           | TESTA                                                                                                                            |
| stack_status         | CREATE_COMPLETE                                                                                                                  |
| stack_status_reason  | Stack CREATE completed successfully                                                                                              |
| template_description | Basic template of two servers, one network and one                                                                               |
|                      | router                                                                                                                           |
| timeout_mins         | None                                                                                                                             |
| updated_time         | None                                                                                                                             |
+----------------------+----------------------------------------------------------------------------------------------------------------------------------+ 

Для большинства случаев вывод команды heat stack-show cлишком большой и подробный. Найти в этом выводе какую-нибудь небольшую, но важную деталь (например, IP-адрес первого сервера) крайне затруднительно. Если интересует только значение плавающего адреса первого сервера, то получить его можно следующей командой, где после имени стека укажите также описанный вывод о публичном IP-адресе:

$ heat output-show TESTA server1_public_ip
"95.213.154.192"

Удаление стека осуществляется при помощи команды heat stack-delete:

$ heat stack-delete TESTA
+--------------------------------------+------------+--------------------+----------------------+
| id                                   | stack_name | stack_status       | creation_time        |
+--------------------------------------+------------+--------------------+----------------------+
| e7ad8ef1-921d-4e70-a203-20dbc32d4a02 | TESTA      | DELETE_IN_PROGRESS | 2014-12-17T18:30:54Z |
+--------------------------------------+------------+--------------------+----------------------+

В ситуации, когда необходимо временно высвободить системные ресурсы, не удаляя при этом сам стек, можно приостановить его работу командой heat action-suspend и вернуть в рабочее состояние позже с помощью команды heat action-resume.

Более подробную информацию можно получить в официальной документации или с помощью команды heat help.