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_tagopenstack.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')# capabilitiesallow_create = Trueallow_fetch = Trueallow_commit = Trueallow_delete = Trueallow_list = True_query_mapping = resource.QueryParameters( 'id', 'name', 'description', 'type', 'domain', 'content', 'private_key', 'marker', 'limit',)# Properties#: Namename = resource.Body('name')#: Idid = resource.Body('id')#: Descriptiondescription = 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 timecreate_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 OTCModuleclass 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 самостоятельно ищет файл для авторизации в
следующих местах:
-
system-wide (/etc/openstack/{clouds,secure}.yaml)
-
Home directory / user space
(~/.config/openstack/{clouds,secure}.yaml)
-
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 fileexport OS_USERNAME="<USER_NAME>"export OS_USER_DOMAIN_NAME=<OTC00000000001000000XYZ>export OS_PASSWORD=<PASSWORD> # optionalexport OS_TENANT_NAME=eu-deexport OS_PROJECT_NAME=<eu-de_PROJECT_NAME>export OS_AUTH_URL=https://iam.eu-de.otc.t-systems.com:443/v3export NOVA_ENDPOINT_TYPE=publicURLexport OS_ENDPOINT_TYPE=publicURLexport CINDER_ENDPOINT_TYPE=publicURLexport OS_VOLUME_API_VERSION=2export OS_IDENTITY_API_VERSION=3export 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:
Если тема интересна, то буду рад поделиться своим опытом по
работе с другими инструментами. Пишите в комментариях, готов
ответить на ваши вопросы!