company_banner

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

    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 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 будет упрощено.

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

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

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


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

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

    image

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

    Спойлер


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

    Спойлер


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

    $ heat output-show TESTA server1_public_ip
    "95.213.154.192"
    

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


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

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

    Заключение


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

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

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

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

    Читателей, которые по тем или иным причинам не могут оставлять комментарии здесь, приглашаем в наш блог.
    • +18
    • 11,2k
    • 1
    Selectel
    95,38
    ИТ-инфраструктура для бизнеса
    Поделиться публикацией

    Похожие публикации

    Комментарии 1

      +4
      Ваши статьи не читаю, но картинки смотрю с удовольствием

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое