Искусство виртуального дирижирования OpenStack: работа с Heat

в 10:46, , рубрики: heat, openstack, selectel, virtual private cloud, VPC, Блог компании Селектел, Облачные вычисления, селектел, системное администрирование

Heat

В предыдущей cтатье мы описали базовые принципы работы с API и консольными утилитами, управляющими отдельными компонентами платформы Openstack (nova, cinder, glance, neutron). Сегодня мы рассмотрим, как с помощью модуля оркестрации Heat можно построить готовую инфраструктуру из виртуальных устройств.

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

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

Прежде чем начинать разговор о конкретных практических аспектах работы с Heat, проясним значения основных понятий — «стек» и «шаблон».

Стеком (англ. 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 routern", 
  "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 будет упрощено.

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

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

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

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

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

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

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

Спойлер

Искусство виртуального дирижирования OpenStack: работа с Heat - 5

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

$ heat output-show TESTA server1_public_ip
"95.213.154.192"

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

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

Мы рассмотрели только наиболее часто используемые, на наш взгляд, операции со стеками (и не касались управления отдельными ресурсами), события, обновление стека во время его работы и других возможностей. Более подробную информацию можно получить в официальной документации или с помощью команды heat help.

Заключение

В этой статье мы познакомились с основными принципами работы модуля оркестрации openstack Heat, который даёт нам дополнительный уровень абстракции при работе с облаком и избавляет от множества рутинных действий.

Разумеется, этим возможности Heat не ограничиваются. Мы не сказали о важной способности передавать создаваемой машине так называемые пользовательские данные (user_data), которые будут выполняться внутри машины при её первичной загрузке. Строго говоря, Heat передаёт данные машине на исполнение не самостоятельно, а через компонент Nova. Но за счёт возможности описывать связи между ресурсами Нeat позволяет не ограничивать условия выполнения передаваемых данных рамками одной машины.

Например, нужно создать несколько машин, одна из которых будет выполнять роль сервера баз данных, а остальные подключаться к ней по IP-адресу. Благодаря использованию шаблонов, мы можем не задумываться о последовательности создания машин и их сетевых настройках. Как только соответствующие ресурсы будут созданы, все необходимые значения, в том числе ip адрес сервера баз данных, будут переданы в user_data.

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

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

Автор: burlunder

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js