Управление инфраструктурой Open Telekom Cloud с помощью Ansible

от автора

Open Telekom Cloud

В этой статье расскажу о нашем опыте работы над развитием инструментов управления инфраструктурой облачного сервиса Open Telekom Cloud, как мы столкнулись с особенностями этого облака, какие решения принимали и какие инструменты использовали.

Open Telekom Cloud – международная публичная облачная платформа, основанная на OpenStack. Платформа идеально подходит для компаний или стартапов, которые работают с европейскими пользователями, чьи персональные данные должны храниться в пределах Евросоюза: сервис разработан Deutsche Telekom и соответствует стандартам защиты данных GDPR (Генеральный регламент о защите персональных данных) EC.

С чего все начиналось

Почти два года назад в поисках специалистов в Россию пришел проект Open Telekom Cloud. Требовалось много людей на автоматизированное тестирование и несколько человек в спецотряд под названием Ecosystems, требования были очень расплывчатые: «Ну, надо знать Python и понимать, как работать с облачными сервисами…»

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

Команда Ecosystems занималась API мониторингом, мониторингом сервисов, созданием модулей для Ansible, разработкой и поддержкой инструментов управления инфраструктурой Open Telekom Cloud. На данный момент она участвует еще и в разработке Terraform Provider, OpenStack SDK, OpenStack Ansible Collections. Во всех наших инструментах (OTC Extensions, Terraform Provider, Ansible Collections) мы стараемся максимально соответствовать OpenStack и переиспользовать существующие решения для него.

С самого начала с Open Telekom Cloud все оказалось довольно интересно. Разработка находится на стороне Huawei, декларировалось, что облако основано полностью на технологии OpenStack. Но Huawei внесли множество своих решений в сервисы. Многие из них были полностью написаны китайскими коллегами, были заметные отличия нашего API от OpenStack API.

Но тогда это нас не сильно волновало. Первой нашей задачей в Ecosystems было создание мониторингов, которые помогут определить качество работы тех или иных сервисов, в сложных сценариях. Например, использовать балансировщик нагрузки для хостов в разных AZ (availability zone), наблюдать за распределением запросов по каждому из хостов. Или следить за отказоустойчивостью того же балансировщика нагрузки в сценарии, когда один или несколько хостов за ним выключаются по тем или иным причинам.

В качестве инструментов для реализации задачи был выбран Ansible и Terraform, провайдер для Terraform уже существовал, и Huawei его в какой-то степени поддерживал. При создании мониторинга начали изучать и использовать Go для различных задач. Например, нужен был быстрый сервер, который не загнется от потока запросов, что в будущем открыло нам новое направление с поддержкой провайдера Terraform. Во время создания мониторинга находились баги в Terraform провайдере, и казалось, что дождаться их решения будет невозможно, никто их исправлять не спешил.

Что ж, раз Terraform стал для нас критичным инструментом, мы отправились в репозиторий с провайдером и начали исправлять баги. В какой-то момент поддержка провайдера со стороны Huawei прекратилась, да и через некоторое время HashiCorp решили всех сторонних провайдеров убрать из своего репозитория.

Тогда мы решили перетащить провайдер в свою организацию на Github, форкаем golangsdk, называем его gophertelekomcloud и переносим провайдер на него (после всех преобразований gophertelekomcloud в итоге стал самостоятельным проектом, указания, что это форк, больше нет). Но это уже другая история…

С начала работы на проекте прошло примерно полгода, из-за провайдера объем работы вырос и стало понятно, что два человека со всем не справятся. Мы набрали в команду толковых ребят, и часть из них стала заниматься развитием и поддержкой Terraform провайдера, часть осталась на мониторингах.

Ansible и коллекции

Опустив некоторые детали, первоначально мониторинг работал примерно так:

Точкой входа был AWX, он вызывал плейбуки с ролью Terraform, результат выполнения Terraform снова передавался в Ansible, и далее выполнялись какие-то сценарии.

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

Но подход к мониторингу поменялся. Мы начинали как самостоятельный проект без ограничений на выбор инструментов и не было задачи интегрироваться в какую-то существующую инфраструктуру, но теперь пришла задача интегрироваться в существующую инфраструктуру API-мониторинга с целью сделать более универсальный единый инструмент. В которой нет AWX и Terraform.

Был только Python и Ansible…

Учитывая, что Open Telekom Cloud не является решением, на 100% совместимым с OpenStack, в нем присутствуют сервисы собственной разработки, например, RDS (Relational Database Service). С помощью Ansible мы не можем построить все необходимые нам ресурсы используя OpenStack SDK и ansible-collections-openstack, в таком виде, чтобы это было легко поддерживать.

Что ж, надо расширять возможности OpenStack SDK, адаптировать под наш проект и писать собственные коллекции. Для коллекций необходимо описание ресурсов, которых нет в OpenStack SDK, для таких ресурсов был создан проект OTC Extensions.

OTC Extensions

Этот проект дополняет и расширяет функции OpenStack SDK для работы с Open Telekom Cloud, так же если он установлен в качестве python package, в OpenStack Client добавляются дополнительные плагины для работы с облаком.

Взаимодействует с:

·         python-openstacksdk

·         python-openstackclient

Структура проекта близка к OpenStack SDK:

otcextensions/     sdk/         compute/             v2/                 server.py                 _proxy.py     tests/         unit/             sdk/                 compute/                     v2/                         test_server.py

Все дополнительные ресурсы унаследованы от базового openstack.resource.Resource, или если мы хотим изменить существующий объект то нужно наследование от него базового класса этого объекта, например, если у openstack.compute.v2.server нет поддержки тэгов или они реализованы иначе:

class Server(server.Server):      def add_tag(self, session, tag):         """Adds a single tag to the resource."""      def remove_tag(self, session, tag):         """Removes a single tag from the specified server."""

И далее патчим Connection в методе load (otcextensions/sdk/__init__.py):

openstack.compute.v2.server.Server.add_tag = server.Server.add_tag openstack.compute.v2.server.Server.remove_tag = server.Server.remove_tag

В итоге наш connection теперь будет работать с кастомными тегами.

Для нового ресурса:

otcextensions/     sdk/         elb/             v2/                 elb_certificate.py                 _proxy.py

В файле elb_certificate.py создаем класс, указываем его url, какие методы поддерживает, какие параметры принимает

class Certificate(resource.Resource): resources_key = 'certificates' base_path = ('/lbaas/certificates')  # capabilities allow_create = True allow_fetch = True allow_commit = True allow_delete = True allow_list = True  _query_mapping = resource.QueryParameters(     'id', 'name', 'description',     'type', 'domain', 'content',     'private_key', 'marker', 'limit', )  # Properties #: Name name = resource.Body('name') #: Id id = resource.Body('id') #: Description description = resource.Body('description') #: Certificate type. type = resource.Body('type') #: Domain name associated with the server certificate. domain = resource.Body('domain') #: Private key of the server certificate. *Type: string* private_key = resource.Body('private_key') #: Public key of the server certificate or CA certificate. *Type: string* content = resource.Body('certificate') #: Administrative status of the certificate. admin_state_up = resource.Body('admin_state_up') #: Creation time create_time = resource.Body('create_time') #: Specifies the project ID. project_id = resource.Body('tenant_id') #: Time when the certificate expires. expire_time = resource.Body('expire_time') #: Time when the certificate was updated. update_time = resource.Body('update_time')

Рядом обязательно должен быть файл _proxy.py, этот класс адаптер предоставляет интерфейс для работы с инстансом Connection, в нем мы описываем методы ресурса:

class Proxy(_proxy.Proxy):     skip_discovery = True      # ======== Certificate ========     def create_certificate(self, **attrs):         return self._create(_certificate.Certificate, **attrs)      def certificates(self, **query):         return self._list(_certificate.Certificate, **query)      def delete_certificate(self, certificate, ignore_missing=True):         return self._delete(_certificate.Certificate, certificate,                             ignore_missing=ignore_missing)      def get_certificate(self, certificate):         return self._get(_certificate.Certificate, certificate)      def update_certificate(self, certificate, **attrs):         return self._update(_certificate.Certificate, certificate, **attrs)      def find_certificate(self, name_or_id, ignore_missing=False):         return self._find(_certificate.Certificate, name_or_id,                           ignore_missing=ignore_missing)

В otcextensions/sdk/__init__.py eсть структура со всеми нестандартными ресурсами — OTC_SERVICES, добавляем наш ресурс по имени папки в которой он находится:

'elb': {     'service_type': 'elb',     'replace_system': True }

OTC_SERVICES так же в методе load, добавляются в Connection:

for (service_name, service) in OTC_SERVICES.items():     if service.get('replace_system', False):         if service['service_type'] in conn._proxies:             del conn._proxies[service['service_type']]     sd = _get_descriptor(service_name)     conn.add_service(sd)

На этом добавление сервиса завершено, мы можем его использовать через OpenStack SDK.

cfg = openstack.config.get_cloud_region(cloud=TEST_CLOUD_NAME) conn = connection.Connection(config=cfg) sdk.register_otc_extensions(conn) cert = conn.elb.create_certificate(     private_key=PRIVATE_KEY,     content=CERTIFICATE,     name=NAME  )

Ansible collections

Окей, ресурсы теперь есть, осталось разобраться как их использовать, есть отличный вариант, создать коллекцию своих модулей и хранить ее в ansible-galaxy, по аналогии с ansible-collections-openstack создаем коллекцию ansible-collection-cloud, которая основана на OTC extensions.

Если модуль, который мы добавляем в коллекцию существует в OpenStack коллекции, то мы стараемся максимально обеспечить обратную совместимость, создавая единый интерфейс для модулей.

Делаем все по гайду (developing-collections):

ansible-collection-cloud/     plugins/         module_utils/             otc.py         modules/             elb_certificate.py             elb_certificate_info.py

В module_utils, храним базовый для всех модулей класс:

class OTCModule:     """Openstack Module is a base class for all Openstack Module classes.      The class has `run` function that should be overriden in child classes,     the provided methods include:     """

В нем создается инстанс Connection, и патчится через OTC extensions, чтобы мы могли использовать кастомные ресурсы.

Все модули делятся на два типа с постфиксом _info возвращают информацию о существующем ресурсе, без него создают/изменяют/удаляют ресурсы.

Например, lb_certificate_info:

from ansible_collections.opentelekomcloud.cloud.plugins.module_utils.otc import OTCModule   class LoadBalancerCertificateInfoModule(OTCModule):     argument_spec = dict(         name=dict(required=False)     )      otce_min_version = '0.10.0'      def run(self):         data = []          if self.params['name']:             raw = self.conn.elb.find_certificate(name_or_id=self.params['name'], ignore_missing=True)             if raw:                 dt = raw.to_dict()                 dt.pop('location')                 data.append(dt)         else:             for raw in self.conn.elb.certificates():                 dt = raw.to_dict()                 dt.pop('location')                 data.append(dt)          self.exit_json(             changed=False,             elb_certificates=data         )   def main():     module = LoadBalancerCertificateInfoModule()     module()   if __name__ == '__main__':     main()

аналогично выполнен и lb_certificate.

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

Установка коллекций

Для начала работы необходимо установить коллекции в окружение, для примера используем venv (venv использовать необязательно, но такой подход имеет свои плюсы):

/$ cd ~ ~$ python3 -m venv ansiblevenv

Активируем окружение:

~$ source ansiblevenv/bin/activate (ansiblevenv) ~$

Установим OpenStack Client, otcextensions и wheel (необязательно):

(ansiblevenv) ~$ pip install wheel (ansiblevenv) ~$ pip install openstackclient (ansiblevenv) ~$ pip install otcextensions

Для работы с коллекциями далее необходимо установить их из Ansible-Galaxy (Ansible-Galaxy содержит множество свободно распространяемых ролей и коллекций, разрабатываемых сообществом). Дополнительно ставим OpenStack коллекцию для нативных ресурсов:

(ansiblevenv) $ ansible-galaxy collection install opentelekomcloud.cloud (ansiblevenv) $ ansible-galaxy collection install openstack.cloud

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

Авторизация

clouds.yaml

OpenStack client/sdk самостоятельно ищет файл для авторизации в следующих местах:

  1. system-wide (/etc/openstack/{clouds,secure}.yaml)

  2. Home directory / user space (~/.config/openstack/{clouds,secure}.yaml)

  3. Current directory (./{clouds,secure}.yaml)

clouds:   otc:     profile: otc     auth:       username: '<USER_NAME>'       password: '<PASSWORD>'       project_name: '<eu-de_project>'       # or project_id: '<123456_PROJECT_ID>'       user_domain_name: 'OTC00000000001000000xxx'       # or user_domain_id: '<123456_DOMAIN_ID>'     account_key: '<AK_VALUE>' # AK/SK pair for access to OBS     secret_key: '<SK_VALUE>'

После того, как файл создан, указываем переменной окружения OS_CLOUD имя конфигурации, в данном случае:

~$ export OS_CLOUD=otc

Имя может быть любым, и один файл может содержать множество конфигураций.

Чтобы проверить, что все сделано правильно, можно запустить любую команду OpenStack клиента:

~$ openstack server list

Если авторизация успешна, то мы получим список серверов:

Чтобы повысить безопасность, можно вынести чувствительную информацию из clouds.yaml. Рядом с файлом clouds.yaml создаем secure.yaml и помещаем туда все, что хотим скрыть:

clouds:   otc:     auth:       password: '<PASSWORD>'

Переменные окружения

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

# .ostackrc file export OS_USERNAME="<USER_NAME>" export OS_USER_DOMAIN_NAME=<OTC00000000001000000XYZ> export OS_PASSWORD=<PASSWORD> # optional export OS_TENANT_NAME=eu-de export OS_PROJECT_NAME=<eu-de_PROJECT_NAME> export OS_AUTH_URL=https://iam.eu-de.otc.t-systems.com:443/v3 export NOVA_ENDPOINT_TYPE=publicURL export OS_ENDPOINT_TYPE=publicURL export CINDER_ENDPOINT_TYPE=publicURL export OS_VOLUME_API_VERSION=2 export OS_IDENTITY_API_VERSION=3 export OS_IMAGE_API_VERSION=2

Создаем переменные:

~$ source .ostackrc

С авторизацией разобрались! Теперь можно полноценно использовать коллекции.

Использование коллекции

Как мы знаем в коллекции два типа модулей: с постфиксом info возвращают информацию о существующем ресурсе, без него создают/изменяют/удаляют ресурсы. Все модули вызываются по полному имени: opentelekom.cloud.*

Все info модули поддерживают поиск как по имени, так и по id ресурса, например:

- name: Get loadbalancer info   opentelekomcloud.cloud.loadbalancer_info:     name: "{{ lb_name_or_id }}"   register: result

Если передано имя ресурса, то в ответе вернется dict с параметрами ресурса, если имя не указано, то появится список всех доступных в проекте ресурсов. Не инфо модули также возвращают dict.

Пример сценария

Для примера использования коллекций создадим файл example.yaml и будем там описывать различные ресурсы. Создадим небольшую инфраструктуру: сеть, сервер и балансировщик нагрузки.

Для создания нативных ресурсов всегда используются OpenStack модули, например, сеть:

--- - name: Create main network   openstack.cloud.network:     name: my_network   register: net  - name: Getting info about external network   openstack.cloud.networks_info:     name: admin_external_net   register: ext_net  - name: Create subnet   openstack.cloud.subnet:     name: my_subnet     network_name: "{{ net.network.name }}"     cidr: 192.168.0.0/16     dns_nameservers:           - 100.125.4.25           - 100.125.129.199   register: subnet  - name: Create router   openstack.cloud.router:     name: "{{ public_router_scenario }}_router"     enable_snat: true     network: "{{ ext_net.openstack_networks[0].id }}"     interfaces:       - "{{ subnet.subnet.name }}"   register: router

Для сервера нам нужен ключ:

- name: Create key pair   openstack.cloud.keypair:     name: bastion_key_pair     public_key_file: "/tmp/keys/public.pub"   register: keypair

Создадим security group, откроем порты 80, 443 и 22 для ssh, также откроем icmp:

- name: Create security group   openstack.cloud.security_group:     name: bastion_secgroup     description: Allow external connections to ssh, http, https and icmp   register: sec_group  - name: Add rules for tcp connection to the security group   openstack.cloud.security_group_rule:     security_group: "{{ sec_group.secgroup.name }}"     protocol: tcp     port_range_min: "{{ item }}"     port_range_max: "{{ item }}"     remote_ip_prefix: 0.0.0.0/0   loop:      - 22     - 80     - 443  - name: Add a rule for icmp connection to the security group   openstack.cloud.security_group_rule:     security_group: "{{ secur_group.secgroup.name }}"     protocol: icmp     port_range_min: -1     port_range_max: -1     remote_ip_prefix: 0.0.0.0/0

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

- name: Create a port for a bastion   openstack.cloud.port:     name: bastion_port     network: net.network.id     security_groups:       - "{{ sec_group.secgroup.name }}"      fixed_ips:        - ip_address: 192.168.200.10   register: port

Для создания сервера тоже используются нативные модули. Например, создадим bastion (это те хосты, которые принято использовать как jump для доступа в недоступные снаружи сети). Здесь также представлен пример инъекции команд при создании сервера через userdata:

- name: Getting information about a current image   openstack.cloud.image_info:     image: Standard_Debian_10_latest   register: image  - name: Create a new instance   openstack.cloud.server:     state: present     name: bastion     flavor: s2.medium.2     key_name: bastion_key_pair     availability_zone: eu-de-01     security_groups:      - "{{ sec_group.secgroup.name }}"     timeout: 200     userdata: |       {%- raw -%}#!/usr/bin/env bash                  #setup ssh service config                  file=/etc/ssh/sshd_config                  cp -p $file $file.old &&                      while read key other; do                          case $key in                          GatewayPorts) other=yes ;;                          AllowTcpForwarding) other=yes ;;                          PubkeyAuthentication) other=yes ;;                          PermitTunnel) other=yes ;;                          esac                          echo "$key $other"                      done <$file.old > $file                  sudo service sshd restart                   mkdir -p /etc/sslcerts/live                  #generate Diffie-Hellman for TLS                  sudo openssl dhparam -out /etc/sslcerts/live/dhparams.pem 2048       {% endraw %}     nics:       - port-name: "{{ port.port.name }}"     boot_from_volume: true     volume_size: 5     image: "{{ image.openstack_image.id }}"     terminate_volume: true     delete_fip: true     auto_ip: true   register: bastion 

Для динамической регистрации хоста используем add_host:

- name: Register nodes   add_host:     name: "{{ bastion.openstack.name }}"     groups: bastions     ansible_host: "{{ bastion.openstack.interface_ip }}"     ansible_ssh_user: linux     ansible_ssh_private_key_file: "/path/to/key"

После создания сервера можно проверить подключение:

- name: Wait for nodes to be up   hosts: bastions   gather_facts: no   tasks:     - name: Wait for nodes to be up       wait_for_connection:         timeout: 250

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

После того, как у нас создана сеть и есть хотя бы один сервер, мы можем создать loadbalancer:

- name: Create loadbalancer   opentelekomcloud.cloud.loadbalancer:     name: my_elastic_loadbalancer     state: present     vip_subnet: "{{ subnet.subet.id }}"     vip_address: 192.168.200.100     auto_public_ip: true   register: loadbalancer

Далее для loadbalancer создаем listener, если протокол https, то сразу можем создать сертификат:

- name: Create listener http   opentelekomcloud.cloud.lb_listener:     state: present     name: my_listener_http     protocol: http     protocol_port: 80     loadbalancer: "{{ loadbalancer.loadbalancer.id }}"   register: listener_http  - name: Create Server certificate   opentelekomcloud.cloud.lb_certificate:     name: my_https_cetificate     content: "{{ some_https_certificate }}"     private_key: "{{ some_loadbalancer_https_key }}"   register: certificate  - name: Create listener https   opentelekomcloud.cloud.lb_listener:     state: present     name: my_listener_https     protocol: terminated_https     protocol_port: 443     loadbalancer: "{{ loadbalancer.loadbalancer.id }}"     default_tls_container_ref: "{{certificate.elb_certificate.id }}"   register: listener_https

Чтобы добавить к балансировщику сервер, необходимо создать пул серверов. Для каждого listener создается отдельный пул:

- name: Create lb pool http   opentelekomcloud.cloud.lb_pool:     state: present     name: my_pool_http     protocol: http     lb_algorithm: round_robin     listener: "{{ listener_http.listener.id }}"   register: lb_pool_http  - name: Create lb pool https   opentelekomcloud.cloud.lb_pool:     state: present     name: my_pool_https     protocol: http     lb_algorithm: round_robin     listener: "{{ listener_https.listener.id }}"   register: lb_pool_https

Добавляем сервер в пул:

- name: Create members for a http pool in the load balancer   opentelekomcloud.cloud.lb_member:     state: present     name: my_member_http     pool: "{{ lb_pool_http.server_group.id }}"     address: 192.168.200.10     protocol_port: http     subnet: "{{ subnet.subet.id }}"   register: members_http  - name: Create members for a https pool in the load balancer   opentelekomcloud.cloud.lb_member:     state: present     name: my_member_https     pool: "{{ lb_pool_https.server_group.id }}"     address: 192.168.200.10     protocol_port: http     subnet: "{{ subnet.subet.id }}"   register: members_https

И, наконец, добавим healthmonitor для каждого пула, чтобы наблюдать за статусом хостов:

- name: Enable health check for http members   opentelekomcloud.cloud.lb_healthmonitor:     state: present     name: http_healthcheck     pool: "{{ lb_pool_http.server_group.id }}"     delay: 1     max_retries: 2     monitor_timeout: 1     type: http  - name: Enable health check for https members   opentelekomcloud.cloud.lb_healthmonitor:     state: present     name: https_healthcheck     pool: "{{ lb_pool_https.server_group.id }}"     delay: 1     max_retries: 2     monitor_timeout: 1     type: http 

Если выполнять плейбук с verbosity, то в консоли мы увидим все параметры создаваемых ресурсов.

В результате на консоли можно увидеть наш сервер, балансировщик нагрузки и все остальные ресурсы:

Таким образом мы перевели инфраструктуру наших мониторингов полностью на Ansible.

Насколько мне известно, в России не одна компания пользуется услугами Huawei для создания собственных облачных сервисов, было бы интересно увидеть в комментариях, приходилось ли им решать подобные вопросы касаемо расширения ванильного OpenStack SDK и как они к этому подходили.

Весь код находится в публичном доступе и хранится на Github:

Если тема интересна, то буду рад поделиться своим опытом по работе с другими инструментами. Пишите в комментариях, готов ответить на ваши вопросы!

ссылка на оригинал статьи https://habr.com/ru/company/deutschetelekomitsolutions/blog/554384/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *