Управление инфраструктурой Open Telekom Cloud с помощью Ansible
- пятница, 7 мая 2021 г. в 00:40:32
В этой статье расскажу о нашем опыте работы над развитием инструментов управления инфраструктурой облачного сервиса 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 провайдера, часть осталась на мониторингах.
Опустив некоторые детали, первоначально мониторинг работал примерно так:
Точкой входа был 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.
Этот проект дополняет и расширяет функции 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-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 поддерживает несколько способов авторизации.
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 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:
Коллекции: ansible-collection-cloud
полный список модулей: modules
в качестве примеров можно использовать integration tests
OTC Extensions: python-otcextensions
Ansible galaxy: https://galaxy.ansible.com/opentelekomcloud/cloud
Open Telekom Cloud docs: https://docs.otc.t-systems.com/
Open Telekom Cloud main page: https://open-telekom-cloud.com/
Terraform Provider: terraform-provider-opentelekomcloud
Драйвера для Rancher, начиная с версии 2.5 включены в релиз:
Open Telekom Cloud CCE cluster driver
Open Telekom Cloud node driver
Если тема интересна, то буду рад поделиться своим опытом по работе с другими инструментами. Пишите в комментариях, готов ответить на ваши вопросы!