Spec-Zone.ru › Ansible

Рекомендации по разработке модулей Ansible для Amazon AWS

Коллекция Ansible AWS (на Galaxy, репозиторий исходного кода) поддерживается рабочей группой Ansible AWS. Дополнительные сведения см. на странице сообщества рабочей группы AWS. Если вы планируете внести вклад в Ansible, добавив модули AWS, для начала стоит связаться с рабочей группой — в частности, потому, что аналогичный модуль может уже разрабатываться.

  • Требования

    • Совместимость с Python
    • Поддержка версий SDK
  • Поддержка существующих модулей

    • Журналы изменений
    • Несовместимые изменения
    • Добавление новых функций
    • Политика выпусков и перенос объединённых PR в предыдущие версии
  • Создание новых модулей AWS

    • Именование модуля
    • Использование boto3 и AnsibleAWSModule
    • Импорт botocore и boto3
    • Поддержка значений module_defaults
    • Поведение модуля
  • Подключение к AWS

    • Общие фрагменты документации для параметров подключения
  • Обработка исключений

    • Использование is_boto3_error_code
    • Использование fail_json_aws()
    • Использование fail_json() без AnsibleAWSModule
  • Ограничение частоты запросов API и постраничная обработка
  • Возвращаемые значения

    • Теги
    • Информационные модули
    • Устаревание возвращаемых значений
  • Работа с JSON-политиками IAM
  • Работа с тегами
  • Вспомогательные функции

    • camel_dict_to_snake_dict
    • snake_dict_to_camel_dict
    • ansible_dict_to_boto3_filter_list
    • boto_exception
    • boto3_tag_list_to_ansible_dict
    • ansible_dict_to_boto3_tag_list
    • get_ec2_security_group_ids_from_names
    • compare_policies
    • compare_aws_tags
  • Интеграционные тесты модулей AWS

    • Пользовательские версии SDK для интеграционных тестов
    • Создание экземпляров EC2 в интеграционных тестах
    • Именование ресурсов в интеграционных тестах
    • Учётные данные AWS для интеграционных тестов
    • Разрешения AWS для интеграционных тестов

      • Устранение неполадок с политиками IAM
      • Неподдерживаемые интеграционные тесты
  • Модульные тесты плагинов AWS

    • Зачем нужны модульные тесты, если у нас есть функциональные тесты?
    • Как сделать код простым?
    • Рекомендации по модульным тестам
    • Как запускать модульные тесты?
  • Форматирование кода

Требования

Совместимость с Python

Содержимое AWS в Ansible 2.9 и выпусках коллекций 1.x поддерживало Python 2.7 и более новые версии.

Начиная с выпусков 2.0 обеих коллекций, поддержка Python 2.7 будет прекращена в соответствии с прекращением поддержки Python 2.7 со стороны AWS. Вклад в обе коллекции для выпусков 2.0 и более поздних можно писать с использованием синтаксиса Python 3.6+.

Поддержка версий SDK

Начиная с выпусков 2.0 обеих коллекций, общая политика заключается в поддержке версий botocore и boto3, выпущенных за 12 месяцев до последнего основного выпуска коллекции, согласно семантическому версионированию (например, 2.0.0, 3.0.0).

Можно добавлять функции и возможности, для которых требуются более новые версии SDK, при условии, что это указано в документации модуля:

DOCUMENTATION = '''
---
module: ec2_vol
options:
  throughput:
    description:
      - Volume throughput in MB/s.
      - This parameter is only valid for gp3 volumes.
      - Valid range is from 125 to 2000.
      - Requires at least botocore version 1.19.27.
    type: int
    version_added: 1.4.0

И обрабатывается с помощью вспомогательного метода botocore_at_least:

if module.params.get('throughput'):
    if not module.botocore_at_least("1.19.27"):
        module.fail_json(msg="botocore >= 1.19.27 is required to set the throughput for a volume")

Начиная с выпусков 4.0 обеих коллекций, поддержка исходного SDK boto полностью прекращена. Модули AWS необходимо писать с использованием SDK botocore и boto3.

Поддержка существующих модулей

Журналы изменений

К любому PR, меняющему функциональность или исправляющему ошибку, необходимо добавить фрагмент журнала изменений. Дополнительную информацию о фрагментах журналов изменений можно найти в Making your PR merge-worthy section of the Ansible Development Cycle documentation<community_changelogs>

Несовместимые изменения

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

Например: - Устаревшую функцию, объявленную в выпуске 3.0.0, можно удалить в выпуске 4.0.0. - Устаревшую функцию, объявленную в выпуске 1.2.0, можно удалить в выпуске 3.0.0.

К несовместимым изменениям относятся: - Удаление параметра. - Изменение параметра на required. - Изменение значения параметра по умолчанию. - Изменение или удаление существующего возвращаемого значения.

Добавление новых функций

Старайтесь сохранять обратную совместимость с версиями boto3/botocore, выпущенными не менее года назад. Это значит, что если вы хотите реализовать функциональность, использующую новую возможность boto3/botocore, ошибка должна возникать только при явном использовании этой возможности, а сообщение должно указывать на отсутствующую возможность и минимальную требуемую версию botocore. (Поддержка возможностей обычно определяется в botocore, а затем используется boto3.)

module = AnsibleAWSModule(
    argument_spec=argument_spec,
    ...
)

if module.params.get('scope') == 'managed':
    module.require_botocore_at_least('1.23.23', reason='to list managed rules')

Политика выпусков и перенос объединённых PR в предыдущие версии

Все PR для amazon.aws и community.aws необходимо сначала объединять в ветвь main. После принятия и объединения PR в ветвь main его можно перенести в стабильные ветви.

Ветвь main используется для подготовки следующей основной версии (X+1) коллекций и может содержать несовместимые изменения.

Общая политика переноса изменений:

  • Новые функции, устаревания и небольшие изменения можно переносить в последний стабильный выпуск.
  • Исправления ошибок можно переносить в два последних стабильных выпуска.
  • Исправления безопасности следует переносить как минимум в два последних стабильных выпуска.

При необходимости в более старые стабильные ветви можно добавлять дополнительные изменения, связанные с CI, чтобы обеспечить его дальнейшую работу.

Самый простой способ перенести PR — добавить к нему метку backport-Y. После объединения PR бот patchback попытается автоматически создать PR для переноса изменений.

Создание новых модулей AWS

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

Если API Amazon разделяют зависимые ресурсы, например корзины S3 и объекты S3, это часто служит хорошим основанием для разделения модулей. Кроме того, ресурсы, связанные отношением «многие ко многим» с другим ресурсом, например управляемые политики IAM и роли IAM, обычно лучше обрабатывать в двух отдельных модулях.

Хотя можно написать модуль s3, который управляет всем, что связано с S3, его тщательное тестирование и сопровождение затруднительны. Аналогично, можно написать модуль для управления базовым ресурсом группы безопасности EC2 и второй модуль для управления правилами этой группы безопасности, однако это может не соответствовать ожиданиям пользователей.

Однозначно правильного ответа нет, но важно всё обдумать; часто Amazon уже продумала это за вас при разработке своих API.

Именование модуля

В именах модулей следует указывать название управляемого ресурса и добавлять префикс с названием API AWS, на котором основан модуль. Если примеров такого префикса ещё нет, хорошее практическое правило — взять за основу имя клиента, используемое в boto3.

Если только название не является общеизвестным сокращением основного компонента AWS (например, VPC или ELB), не сокращайте его дополнительно и не придумывайте новые сокращения самостоятельно.

Если API AWS в основном управляет одним ресурсом, модуль для управления этим ресурсом можно назвать просто по имени API. Однако для ясности рассмотрите варианты instance или cluster, если Amazon использует эти названия.

Примеры:

  • ec2_instance
  • s3_object (ранее назывался aws_s3, но в основном предназначен для работы с объектами S3)
  • elb_classic_lb (ранее ec2_elb_lb, но относится к API ELB, а не EC2)
  • networkfirewall_rule_group
  • networkfirewall (этот модуль можно было бы назвать networkfirewall_firewall, но второе слово «firewall» избыточно, а API предназначен для создания таких ресурсов брандмауэра)

Примечание. До разделения коллекций и Ansible Core часто использовался префикс aws_, чтобы различать сервисы с общими названиями, например aws_secret. Теперь в этом нет необходимости, а префикс aws_ зарезервирован для сервисов с очень широким охватом, когда ссылка на API AWS может вызвать путаницу. Например, aws_region_info подключается к EC2, но предоставляет общую информацию о регионах, включённых в учётной записи для всех сервисов.

Использование boto3 и AnsibleAWSModule

Во всех новых модулях AWS необходимо использовать boto3/botocore и AnsibleAWSModule.

AnsibleAWSModule значительно упрощает обработку исключений и управление библиотеками, сокращая объём шаблонного кода. Если использовать AnsibleAWSModule в качестве базового класса невозможно, необходимо задокументировать причину и запросить исключение из этого правила.

Импорт botocore и boto3

Модуль ansible_collections.amazon.aws.plugins.module_utils.botocore автоматически импортирует boto3 и botocore. Если boto3 отсутствует в системе, переменной HAS_BOTO3 будет присвоено значение False. Обычно это означает, что модулям не нужно напрямую импортировать boto3. При использовании AnsibleAWSModule проверять HAS_BOTO3 не нужно — модуль выполняет эту проверку сам:

from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule
try:
    import botocore
except ImportError:
    pass  # handled by AnsibleAWSModule

или:

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import missing_required_lib
from ansible_collections.amazon.aws.plugins.module_utils.botocore import HAS_BOTO3
try:
    import botocore
except ImportError:
    pass  # handled by imported HAS_BOTO3

def main():
    if not HAS_BOTO3:
        module.fail_json(missing_required_lib('botocore and boto3'))

Поддержка значений module_defaults

Существующие модули AWS поддерживают использование module_defaults для общих параметров аутентификации. Чтобы добавить такую же поддержку новому модулю, добавьте запись для него в meta/runtime.yml. Эти записи имеют следующий вид:

action_groups:
  aws:
     ...
     example_module

Поведение модуля

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

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

Пример с tags и purge_tags см. в разделе Dealing with tags<ansible_collections.amazon.aws.docsite.dev_tags>.

Подключение к AWS

AnsibleAWSModule предоставляет вспомогательные методы resource и client для получения подключений boto3. Они обрабатывают некоторые менее распространённые параметры подключения, например токены безопасности и профили boto.

При использовании базового AnsibleModule следует применять get_aws_connection_info, а затем boto3_conn для подключения к AWS, поскольку они обрабатывают тот же набор параметров подключения.

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

Ниже показан пример подключения к EC2. Обратите внимание: в отличие от boto, здесь нет обработки исключений NoAuthHandlerFound, как в boto. Вместо этого при использовании подключения будет выброшено исключение AuthFailure. Чтобы перехватывать ошибки авторизации, проверки параметров и разрешений, следует при каждом вызове подключения boto3 перехватывать исключения ClientError и BotoCoreError. См. раздел об обработке исключений:

module.client('ec2')

или для ресурса EC2 более высокого уровня:

module.resource('ec2')

Пример подключения в старом стиле для модулей на основе AnsibleModule, а не AnsibleAWSModule:

region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params)
region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True)
connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params)

Общие фрагменты документации для параметров подключения

Почти во все модули AWS следует включать четыре общих фрагмента документации:

  • boto3 — содержит минимальные требования к коллекции
  • common.modules — содержит общие параметры подключения boto3
  • region.modules — содержит общий параметр региона, необходимый для многих API AWS
  • tags — содержит общие параметры тегирования

Используйте эти фрагменты вместо повторного описания свойств, чтобы обеспечить согласованность и задокументировать менее распространённые параметры подключения. Например:

DOCUMENTATION = '''
module: my_module
# some lines omitted here
extends_documentation_fragment:
    - amazon.aws.boto3
    - amazon.aws.common.modules
    - amazon.aws.region.modules
'''

Для других типов плагинов формат фрагментов документации немного отличается; для них следует использовать следующие фрагменты:

  • boto3 — содержит минимальные требования к коллекции
  • common.plugins — содержит общие параметры подключения boto3
  • region.plugins — содержит общий параметр региона, необходимый для многих API AWS
  • tags — содержит общие параметры тегирования

Используйте эти фрагменты вместо повторного описания свойств, чтобы обеспечить согласованность и задокументировать менее распространённые параметры подключения. Например:

DOCUMENTATION = '''
module: my_plugin
# some lines omitted here
extends_documentation_fragment:
    - amazon.aws.boto3
    - amazon.aws.common.plugins
    - amazon.aws.region.plugins
'''

Обработка исключений

Любые вызовы boto3 или botocore следует оборачивать в блок try. Если возникает исключение, его можно обработать несколькими способами.

  • Catch the general ClientError or look for a specific error code with

    is_boto3_error_code.

  • Используйте aws_module.fail_json_aws() для стандартного сообщения об ошибке модуля.
  • Повторите попытку с помощью AWSRetry.
  • Используйте fail_json(), чтобы сообщить об ошибке без использования AnsibleAWSModule.
  • Реализуйте собственную обработку, если знаете, как следует поступить в данном случае.

Дополнительные сведения об обработке исключений botocore см. в документации botocore об ошибках.

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

Чтобы использовать ansible_collections.amazon.aws.plugins.module_utils.botocore.is_boto3_error_code для перехвата одного кода ошибки AWS, вызовите её вместо ClientError в блоке except. В этом примере здесь будет перехвачен только код ошибки InvalidGroup.NotFound, а все остальные ошибки будут переданы для обработки в другую часть программы.

try:
    info = connection.describe_security_groups(**kwargs)
except is_boto3_error_code('InvalidGroup.NotFound'):
    pass
do_something(info)  # do something with the info that was successfully returned

Использование fail_json_aws()

В AnsibleAWSModule есть специальный метод module.fail_json_aws() для удобного вывода сведений об исключениях. Вызовите его для исключения: он сообщит об ошибке вместе с трассировкой стека, доступной в подробном режиме Ansible.

Для всех новых модулей следует использовать AnsibleAWSModule, если это возможно.

from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule

# Set up module parameters
# module params code here

# Connect to AWS
# connection code here

# Make a call to AWS
name = module.params.get('name')
try:
    result = connection.describe_frooble(FroobleName=name)
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
    module.fail_json_aws(e, msg="Couldn't obtain frooble %s" % name)

Обычно здесь можно перехватывать все стандартные исключения, однако, если вы ожидаете исключения, отличные от исключений botocore, необходимо проверить, что всё работает как ожидается.

Если требуется выполнить действие в зависимости от возвращённой boto3 ошибки, используйте код ошибки и вспомогательную функцию is_boto3_error_code().

# Make a call to AWS
name = module.params.get('name')
try:
    result = connection.describe_frooble(FroobleName=name)
except is_boto3_error_code('FroobleNotFound'):
    workaround_failure()  # This is an error that we can work around
except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:  # pylint: disable=duplicate-except
    module.fail_json_aws(e, msg="Couldn't obtain frooble %s" % name)

Использование fail_json() без AnsibleAWSModule

При возникновении исключения boto3 предоставляет много полезной информации, поэтому передавайте её пользователю вместе с сообщением.

from ansible_collections.amazon.aws.plugins.module_utils.botocore import HAS_BOTO3
try:
    import botocore
except ImportError:
    pass  # caught by imported HAS_BOTO3

# Connect to AWS
# connection code here

# Make a call to AWS
name = module.params.get('name')
try:
    result = connection.describe_frooble(FroobleName=name)
except botocore.exceptions.ClientError as e:
    module.fail_json(msg="Couldn't obtain frooble %s: %s" % (name, str(e)),
                     exception=traceback.format_exc(),
                     **camel_dict_to_snake_dict(e.response))

Примечание. Мы используем str(e) вместо e.message, поскольку последний вариант не работает с python3.

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

# Make a call to AWS
name = module.params.get('name')
try:
    result = connection.describe_frooble(FroobleName=name)
except botocore.exceptions.ClientError as e:
    if e.response['Error']['Code'] == 'FroobleNotFound':
        workaround_failure()  # This is an error that we can work around
    else:
        module.fail_json(msg="Couldn't obtain frooble %s: %s" % (name, str(e)),
                         exception=traceback.format_exc(),
                         **camel_dict_to_snake_dict(e.response))
except botocore.exceptions.BotoCoreError as e:
    module.fail_json_aws(e, msg="Couldn't obtain frooble %s" % name)

Ограничение частоты запросов API и постраничная обработка

Для методов, возвращающих большое количество результатов, boto3 часто предоставляет постраничные итераторы. Если у вызываемого метода есть параметры NextToken или Marker, стоит проверить, существует ли для него постраничный итератор (в верхней части справочной страницы каждого сервиса boto3 есть ссылка на раздел Paginators, если сервис их поддерживает). Чтобы использовать постраничный итератор, получите объект итератора, вызовите paginator.paginate с нужными аргументами, а затем вызовите build_full_result.

При частых вызовах API AWS может сработать ограничение частоты запросов; для реализации задержки перед повтором можно использовать декоратор AWSRetry. Обработка исключений может помешать корректной работе повторных попыток (для работы AWSRetry требуется перехватывать исключения об ограничении частоты запросов), поэтому сначала нужно задать функцию задержки, а затем обернуть её обработчиком исключений.

Можно использовать стратегии exponential_backoff или jittered_backoff — подробнее см. в модуле cloud module_utils ()/lib/ansible/module_utils/cloud.py) и в блоге по архитектуре AWS.

Сочетание этих двух подходов выглядит так:

@AWSRetry.jittered_backoff(retries=5, delay=5)
def describe_some_resource_with_backoff(client, **kwargs):
     paginator = client.get_paginator('describe_some_resource')
     return paginator.paginate(**kwargs).build_full_result()['SomeResource']

def describe_some_resource(client, module):
    filters = ansible_dict_to_boto3_filter_list(module.params['filters'])
    try:
        return describe_some_resource_with_backoff(client, Filters=filters)
    except botocore.exceptions.ClientError as e:
        module.fail_json_aws(e, msg="Could not describe some resource")

До Ansible 2.10, если вызов базового API describe_some_resources вызывал исключение ResourceNotFound, AWSRetry использовал это как сигнал повторять попытку, пока исключение не перестанет возникать (это позволяет при создании ресурса просто повторять попытки, пока он не появится). Поведение по умолчанию изменилось, и теперь его необходимо запрашивать явно. Для этого можно использовать аргумент catch_extra_error_codes декоратора.

@AWSRetry.jittered_backoff(retries=5, delay=5, catch_extra_error_codes=['ResourceNotFound'])
def describe_some_resource_retry_missing(client, **kwargs):
     return client.describe_some_resource(ResourceName=kwargs['name'])['Resources']

def describe_some_resource(client, module):
    name = module.params.get['name']
    try:
        return describe_some_resource_with_backoff(client, name=name)
    except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
        module.fail_json_aws(e, msg="Could not describe resource %s" % name)

Чтобы упростить использование AWSRetry, теперь его можно применять к клиенту, возвращаемому функцией AnsibleAWSModule. К любому вызову клиента. Чтобы добавить повторные попытки для клиента, создайте клиент:

module.client('ec2', retry_decorator=AWSRetry.jittered_backoff(retries=10))

Для любого вызова этого клиента можно использовать декоратор, переданный во время вызова с помощью аргумента aws_retry. По умолчанию повторные попытки не используются.

ec2 = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff(retries=10))
ec2.describe_instances(InstanceIds=['i-123456789'], aws_retry=True)

# equivalent with normal AWSRetry
@AWSRetry.jittered_backoff(retries=10)
def describe_instances(client, **kwargs):
    return ec2.describe_instances(**kwargs)

describe_instances(module.client('ec2'), InstanceIds=['i-123456789'])

Вызов будет повторён указанное количество раз, поэтому функции, выполняющие вызов, не нужно оборачивать декоратором задержки.

Также можно настроить параметры retries, delay и max_delay, используемые API AWSRetry.jittered_backoff, с помощью параметров модуля. В качестве примера можно посмотреть модуль cloudformation <cloudformation_module>.

To make all Amazon modules uniform, prefix the module param with backoff_, so retries becomes backoff_retries

и аналогично для backoff_delay и backoff_max_delay.

Возвращаемые значения

При вызове boto3 вы, вероятно, получите полезные сведения, которые следует вернуть из модуля. Помимо сведений о самом вызове, будут также доступны метаданные ответа. Их тоже можно вернуть пользователю — они могут оказаться полезными.

Большинство ключей boto3 возвращает в CamelCase. В Ansible для имён переменных и их использования применяются стандарты Python. Вспомогательная функция camel_dict_to_snake_dict позволяет легко преобразовать ответ boto3 в snake_case. Она находится в module_utils/common/dict_transformations.

Следует использовать эту вспомогательную функцию и не менять имена значений, возвращаемых Boto3. Например, если boto3 возвращает значение с именем ‘SecretAccessKey’, не переименовывайте его в ‘AccessKey’.

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

# Make a call to AWS
resource = connection.aws_call()

# Convert resource response to snake_case
snaked_resource = camel_dict_to_snake_dict(resource, ignore_list=['Tags'])

# Return the resource details to the user without modifying tags
module.exit_json(changed=True, some_resource=snaked_resource)

Примечание. Возвращаемый ключ с данными конкретного ресурса (выше — some_resource) должен быть разумным приближением к названию ресурса. Например, volume для ec2_vol, volumes для ec2_vol_info.

Теги

Теги следует возвращать в виде словаря пар «ключ: значение», где ключ — это ключ тега, а значение — его значение. Однако следует учитывать, что boto3 часто возвращает теги в виде списка словарей.

В module_utils/ec2.py есть вспомогательная функция boto3_tag_list_to_ansible_dict (подробно рассматривается ниже в разделе «Вспомогательные функции»), которая позволяет легко преобразовать возвращаемый boto3 список тегов в нужный словарь тегов, возвращаемый модулем.

Ниже приведён полный пример получения результата вызова AWS и возврата ожидаемых значений:

# Make a call to AWS
result = connection.aws_call()

# Make result snake_case without modifying tags
snaked_result = camel_dict_to_snake_dict(result, ignore_list=['Tags'])

# Convert boto3 list of dict tags to just a dict of tags
snaked_result['tags'] = boto3_tag_list_to_ansible_dict(result.get('tags', []))

# Return the result to the user
module.exit_json(changed=True, **snaked_result)

Информационные модули

Информационные модули, возвращающие сведения о нескольких ресурсах, должны возвращать список словарей, каждый из которых содержит сведения об отдельном ресурсе (например, security_groups в ec2_group_info).

Если модуль _info возвращает сведения только об одном ресурсе (например, ec2_tag_info), следует возвращать отдельный словарь, а не список словарей.

Если модуль _info не находит экземпляров, следует возвращать пустой список ‘[]’.

Ключи в возвращаемых словарях должны соответствовать приведённым выше рекомендациям и использовать snake_case. Если возвращаемое значение можно использовать в качестве параметра соответствующего основного модуля, ключ должен совпадать с именем параметра или его псевдонимом.

Ниже приведён пример неправильного использования образца информационного модуля вместе с соответствующим основным модулем:

"security_groups": {
    {
        "description": "Created by ansible integration tests",
        "group_id": "sg-050dba5c3520cba71",
        "group_name": "ansible-test-87988625-unknown5c5f67f3ad09-icmp-1",
        "ip_permissions": [],
        "ip_permissions_egress": [],
        "owner_id": "721066863947",
        "tags": [
            {
                "Key": "Tag_One"
                "Value": "Tag_One_Value"
            },
        ],
        "vpc_id": "vpc-0cbc2380a326b8a0d"
    }
}

В примере вывода выше показано несколько ошибок в образце информационного модуля групп безопасности: * security_groups — это словарь словарей, а не список словарей. * tags, по всей видимости, возвращается напрямую из boto3, поскольку это список словарей.

Ниже показан пример вывода после исправления ошибок.

"security_groups": [
    {
        "description": "Created by ansible integration tests",
        "group_id": "sg-050dba5c3520cba71",
        "group_name": "ansible-test-87988625-unknown5c5f67f3ad09-icmp-1",
        "ip_permissions": [],
        "ip_permissions_egress": [],
        "owner_id": "721066863947",
        "tags": {
            "Tag_One": "Tag_One_Value",
        },
        "vpc_id": "vpc-0cbc2380a326b8a0d"
    }
]

Устаревание возвращаемых значений

Если требуется изменить текущие возвращаемые значения, новые/«правильные» ключи следует возвращать в дополнение к существующим, чтобы сохранить совместимость с имеющимися плейбуками. Для заменяемых возвращаемых значений следует объявить устаревание, назначив дату не менее чем через два года, на первое число месяца.

Например:

# Deprecate old `iam_user` return key to be replaced by `user` introduced on 2022-04-10
module.deprecate("The 'iam_user' return key is deprecated and will be replaced by 'user'. Both values are returned for now.",
                 date='2024-05-01', collection_name='community.aws')

Работа с JSON-политиками IAM

Если модуль принимает JSON-политики IAM, укажите для типа в спецификации модуля значение ‘json’. Например:

argument_spec.update(
    dict(
        policy=dict(required=False, default=None, type='json'),
    )
)

Обратите внимание: AWS, скорее всего, вернёт политику не в том порядке, в котором она была отправлена. Поэтому используйте вспомогательную функцию compare_policies, учитывающую это различие.

compare_policies принимает два словаря, рекурсивно сортирует их и преобразует в хешируемый вид для сравнения, а затем возвращает True, если они различаются.

from ansible_collections.amazon.aws.plugins.module_utils.iam import compare_policies

import json

# some lines skipped here

# Get the policy from AWS
current_policy = json.loads(aws_object.get_policy())
user_policy = json.loads(module.params.get('policy'))

# Compare the user submitted policy to the current policy ignoring order
if compare_policies(user_policy, current_policy):
    # Update the policy
    aws_object.set_policy(user_policy)
else:
    # Nothing to do
    pass

Работа с тегами

В AWS предусмотрена возможность присваивать ресурсу теги. Обычно API boto3 предоставляет отдельные вызовы для присвоения и удаления тегов. Например, в API EC2 есть вызовы create_tags и delete_tags.

При добавлении поддержки тегов модули Ansible AWS должны иметь параметр tags со значением по умолчанию None и параметр purge_tags со значением по умолчанию True.

argument_spec.update(
    dict(
        tags=dict(type='dict', required=False, default=None),
        purge_tags=dict(type='bool', required=False, default=True),
    )
)

Если параметр purge_tags имеет значение True и параметр tags явно задан в задаче, все теги, явно не указанные в tags, следует удалить.

Если параметр tags не задан, теги изменять не следует, даже если purge_tags имеет значение True. Это означает, что для удаления всех тегов необходимо явно задать для tags пустой словарь {} в задаче Ansible.

Для упрощения работы с тегами предусмотрена вспомогательная функция compare_aws_tags. Она сравнивает два словаря — текущих и желаемых тегов — и возвращает теги для добавления и удаления. Подробнее см. в разделе о вспомогательных функциях ниже.

При добавлении поддержки тегов следует также включить фрагмент документации amazon.aws.tags.

Вспомогательные функции

Помимо функций подключения в модуле Ansible ec2.py module_utils, ниже описаны некоторые другие полезные функции.

camel_dict_to_snake_dict

boto3 возвращает результаты в виде dict. Ключи dict записаны в формате CamelCase. Чтобы соответствовать формату Ansible, эта функция преобразует ключи в snake_case.

camel_dict_to_snake_dict принимает необязательный параметр ignore_list — список ключей, которые не нужно преобразовывать (обычно это полезно для dict tags, дочерние ключи которого должны сохранить исходный регистр)

Ещё один необязательный параметр — reversible. По умолчанию HTTPEndpoint преобразуется в http_endpoint, которое затем будет преобразовано функцией snake_dict_to_camel_dict в HttpEndpoint. Передача reversible=True преобразует HTTPEndpoint в h_t_t_p_endpoint, которое преобразуется обратно в HTTPEndpoint.

snake_dict_to_camel_dict

snake_dict_to_camel_dict преобразует ключи в snake_case в ключи в camelCase. По умолчанию, поскольку функция изначально была создана для ECS, используется стиль dromedaryCase. Необязательный параметр capitalize_first, значение которого по умолчанию — False, позволяет преобразовать ключи в CamelCase.

ansible_dict_to_boto3_filter_list

Преобразует список фильтров Ansible в совместимый с boto3 список dict. Это полезно для любых модулей boto3 _facts.

boto_exception

Передайте этой функции исключение, возвращённое boto или boto3, и она извлечёт из него сообщение единообразным способом.

Устарело: используйте вместо этого AnsibleAWSModule из fail_json_aws.

boto3_tag_list_to_ansible_dict

Преобразует список тегов boto3 в dict Ansible. По умолчанию boto3 возвращает теги в виде списка dict с ключами ‘Key’ и ‘Value’. При вызове функции эти имена ключей можно переопределить. Например, если вы уже преобразовали список тегов в camel_case, можно передать имена ключей в нижнем регистре, то есть ‘key’ и ‘value’.

Эта функция преобразует список в единый dict, где ключ dict — это ключ тега, а значение dict — значение тега.

ansible_dict_to_boto3_tag_list

Противоположное преобразование. Преобразует dict Ansible в список тегов boto3, состоящий из dict. Если имена ‘Key’ и ‘Value’ не подходят, их также можно переопределить.

get_ec2_security_group_ids_from_names

Передайте этой функции список имён групп безопасности или сочетание имён и идентификаторов групп безопасности, и она вернёт список идентификаторов. Также следует передать идентификатор VPC, если он известен, поскольку имена групп безопасности не обязательно уникальны для разных VPC.

compare_policies

Передайте два dict с политиками, чтобы проверить, есть ли между ними значимые различия; функция вернёт true, если они есть. Перед сравнением функция рекурсивно сортирует dict и преобразует их в хешируемый вид.

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

compare_aws_tags

Передайте два dict с тегами и необязательный параметр purge. Функция вернёт dict с парами ключей, которые нужно изменить, и список имён ключей тегов, которые нужно удалить. По умолчанию purge имеет значение True. Если purge имеет значение False, существующие теги не будут изменены.

Эта функция полезна при использовании функций boto3 add_tags и remove_tags. Перед вызовом этой функции обязательно используйте другую вспомогательную функцию boto3_tag_list_to_ansible_dict, чтобы получить подходящий dict тегов. Поскольку API AWS неоднородны (например, EC2 отличается от Lambda), для одних сервисов это будет работать без изменений (Lambda), а для других может потребоваться предварительно изменить полученные значения (например, EC2, где теги для удаления должны иметь вид [{'Key': key1}, {'Key': key2}]).

Интеграционные тесты модулей AWS

Для всех новых модулей AWS следует добавлять интеграционные тесты, чтобы выявлять любые изменения в API AWS, влияющие на модуль. Как минимум тесты должны охватывать основные вызовы API и проверять наличие задокументированных возвращаемых значений в результате работы модуля.

Общие сведения о запуске интеграционных тестов см. на странице «Интеграционные тесты» руководства по разработке модулей, особенно в разделе о настройке тестов для облачных сервисов.

Интеграционные тесты для вашего модуля следует добавить в test/integration/targets/MODULE_NAME.

Также необходимо добавить файл aliases в test/integration/targets/MODULE_NAME/aliases. Этот файл выполняет две задачи. Во-первых, он указывает, что тест относится к AWS, благодаря чему тестовая инфраструктура предоставляет учётные данные AWS во время запуска теста. Во-вторых, добавление теста в группу обеспечивает его запуск при непрерывной интеграции.

Тесты для новых модулей следует добавить в группу cloud/aws. Обычно достаточно скопировать существующий файл aliases, например файл aliases для тестов aws_s3.

Пользовательские версии SDK для интеграционных тестов

По умолчанию интеграционные тесты запускаются с самой ранней поддерживаемой версией AWS SDK. Список текущих поддерживаемых версий приведён в tests/integration/constraints.txt; его не следует обновлять. Если модулю требуется более поздняя версия SDK, её можно установить, указав зависимость от роли setup_botocore_pip и задав переменную botocore_version в файле meta/main.yml для ваших тестов.

dependencies:
  - role: setup_botocore_pip
    vars:
      botocore_version: "1.20.24"

Создание экземпляров EC2 в интеграционных тестах

При запуске интеграционные тесты получают aws_region в качестве дополнительной переменной. Все создаваемые ресурсы, включая экземпляры EC2, следует создавать в этом регионе. Поскольку AMI зависят от региона, можно подключить роль, которая запросит через API подходящий AMI и установит факт ec2_ami_id. Чтобы подключить эту роль, добавьте роль setup_ec2_facts в качестве зависимости в файл meta/main.yml для ваших тестов.

dependencies:
  - role: setup_ec2_facts

Затем в тестах можно использовать факт ec2_ami_id.

- name: Create launch configuration 1
  community.aws.ec2_lc:
    name: '{{ resource_prefix }}-lc1'
    image_id: '{{ ec2_ami_id }}'
    assign_public_ip: yes
    instance_type: '{{ ec2_instance_type }}'
    security_groups: '{{ sg.group_id }}'
    volumes:
      - device_name: /dev/xvda
        volume_size: 10
        volume_type: gp2
        delete_on_termination: true

Чтобы повысить воспроизводимость результатов тестов в разных регионах, тестам следует использовать эту роль и предоставляемый ею факт для выбора AMI.

Именование ресурсов в интеграционных тестах

AWS устанавливает различные ограничения на имена ресурсов. По возможности в имена ресурсов следует включать строку, обеспечивающую их уникальность для теста.

Инструмент ansible-test, используемый для запуска интеграционных тестов, предоставляет две полезные дополнительные переменные: resource_prefix и tiny_prefix. Они уникальны для набора тестов, и их обычно следует использовать в составе имени. resource_prefix создаёт префикс на основе хоста, на котором выполняется тест. Иногда из-за этого имя ресурса может превысить допустимую в AWS длину. В таких случаях tiny_prefix предоставит случайно сгенерированный 12-символьный префикс.

Учётные данные AWS для интеграционных тестов

Тестовая инфраструктура запускает тесты с подходящими учётными данными AWS, которые доступны тесту через следующие переменные:

  • aws_region
  • aws_access_key
  • aws_secret_key
  • security_token

Поэтому при каждом вызове модулей AWS в тесте необходимо задавать эти параметры. Чтобы не дублировать их при каждом вызове, рекомендуется использовать module_defaults. Например:

- name: set connection information for aws modules and run tasks
  module_defaults:
    group/aws:
      aws_access_key: "{{ aws_access_key }}"
      aws_secret_key: "{{ aws_secret_key }}"
      security_token: "{{ security_token | default(omit) }}"
      region: "{{ aws_region }}"

  block:

  - name: Do Something
    ec2_instance:
      ... params ...

  - name: Do Something Else
    ec2_instance:
      ... params ...

Разрешения AWS для интеграционных тестов

Как описано в руководстве по интеграционным тестам, в mattclay/aws-terminator определены политики IAM с разрешениями, необходимыми для запуска интеграционного теста AWS.

Если ваш модуль взаимодействует с новым сервисом или ему по другой причине требуются новые разрешения, тесты завершатся с ошибкой после отправки запроса на слияние, а Ansibullbot пометит ваш PR как требующий доработки. Мы не предоставляем автоматически дополнительные разрешения ролям, используемым в сборках непрерывной интеграции. Чтобы добавить необходимые разрешения, отправьте запрос на слияние в mattclay/aws-terminator.

Если ваши тесты завершаются с ошибкой, внимательно проверьте, что её причиной является только отсутствие разрешений. Исключив другие причины сбоя, добавьте комментарий с тегом ready_for_review и укажите, что сбой вызван отсутствием разрешений.

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

Есть два способа определить, какие разрешения IAM нужны, чтобы тесты в вашем PR прошли:

  • Начните с максимально разрешительной политики IAM, запустите тесты, чтобы собрать сведения об используемых ими ресурсах, а затем сформируйте политику на основе полученных данных. Этот способ подходит только для модулей, использующих AnsibleAWSModule.
  • Начните с наименее разрешительной политики IAM, запустите тесты, чтобы обнаружить сбой, добавьте разрешения для ресурса, устранение проблемы с которым приведёт к успешному прохождению теста, а затем повторите процесс. Если модуль использует AnsibleModule вместо AnsibleAWSModule, необходимо воспользоваться этим способом.

Чтобы начать с максимально разрешительной политики IAM:

  1. Создайте политику IAM, разрешающую все действия (задайте для Action и Resource значение *).
  2. Запустите тесты локально с этой политикой. Для модулей на основе AnsibleAWSModule параметр debug_botocore_endpoint_logs автоматически устанавливается в значение yes, поэтому после PLAY RECAP вы должны увидеть список ACTIONS AWS со всеми использованными разрешениями. Если в тестах используется модуль boto/AnsibleModule, начните с наименее разрешительной политики (см. ниже).
  3. Измените политику, разрешив только действия, используемые тестами. По возможности ограничьте учётную запись, регион и префикс. Подождите несколько минут, пока изменения политики вступят в силу.
  4. Запустите тесты ещё раз от имени пользователя или роли, которым разрешена только новая политика.
  5. Если тесты завершились с ошибкой, выясните её причину (см. советы ниже), измените политику, запустите тесты ещё раз и повторяйте процесс, пока тесты не пройдут с ограничительной политикой.
  6. Создайте запрос на слияние с предложением минимально необходимой политики для политик CI.

Чтобы начать с наименее разрешительной политики IAM:

  1. Запустите интеграционные тесты локально без разрешений IAM.
  2. Изучите ошибку, возникшую при сбое тестов.
    1. Если в сообщении об ошибке указано действие, использованное в запросе, добавьте это действие в политику.
    2. Если в сообщении об ошибке не указано действие, использованное в запросе:
      • Обычно действие представляет собой название метода в CamelCase. Например, для клиента ec2 методу describe_security_groups соответствует действие ec2:DescribeSecurityGroups.
      • Найдите нужное действие в документации.
    3. Если в сообщении об ошибке указан ARN ресурса, использованного в запросе, ограничьте действие этим ресурсом.
    4. Если в сообщении об ошибке не указан ARN ресурса:
      • Изучите документацию и определите, можно ли ограничить действие конкретным ресурсом.
      • Если действие можно ограничить, используйте документацию, чтобы сформировать ARN, и добавьте его в политику.
  3. Добавьте действие или ресурс, вызвавший сбой, в политику IAM. Подождите несколько минут, пока изменения политики вступят в силу.
  4. Запустите тесты ещё раз, назначив эту политику пользователю или роли.
  5. Если тесты снова завершаются с ошибкой на том же этапе и с тем же сообщением, необходимо выяснить причину (см. советы ниже). Если первый тест пройдёт, повторите шаги 2 и 3 для следующей ошибки. Продолжайте этот процесс, пока тесты не пройдут с ограничительной политикой.
  6. Создайте запрос на слияние с предложением минимально необходимой политики для политик CI.

Устранение неполадок с политиками IAM

  • После изменения политики подождите несколько минут, чтобы изменения вступили в силу, и только потом запускайте тесты повторно.
  • Используйте симулятор политик, чтобы проверить, разрешено ли каждое действие в вашей политике (с ограничением по ресурсу, если применимо).
  • Если вы ограничиваете действия определёнными ресурсами, временно замените ресурсы на *. Если тесты проходят с ресурсами-шаблонами, проблема связана с определением ресурса в вашей политике.
  • Если приведённые выше действия по устранению неполадок не помогли разобраться в проблеме, возможно, AWS использует дополнительные неуказанные ресурсы и действия.
  • Изучите политику AWS FullAccess для этого сервиса, чтобы найти подсказки.
  • Повторно прочитайте документацию AWS, особенно списки действий, ресурсов и ключей условий для различных сервисов AWS.
  • В качестве дополнительного источника для диагностики изучите документацию cloudonaut.
  • Воспользуйтесь поисковой системой.
  • Задайте вопрос в чате #ansible-aws (через Matrix на ansible.im или IRC на irc.libera.chat).

Неподдерживаемые интеграционные тесты

Существует ограниченное число причин, по которым запуск интеграционных тестов модуля в CI может быть нецелесообразен. В таких случаях добавьте ключевое слово unsupported в файл aliases в test/integration/targets/MODULE_NAME/aliases.

Тесты следует помечать как неподдерживаемые в следующих случаях: 1) выполнение тестов занимает более 10 или 15 минут; 2) тесты создают дорогостоящие ресурсы; 3) тесты создают встроенные политики; 4) для тестов необходимы внешние ресурсы; 5) тесты управляют политиками безопасности на уровне учётной записи, например политикой паролей или AWS Organizations.

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

Неподдерживаемые интеграционные тесты не запускаются автоматически в CI. Однако необходимые политики должны быть доступны, чтобы тесты можно было запустить вручную при проверке PR или подготовке исправления.

Модульные тесты для плагинов AWS

Зачем нужны модульные тесты, если у нас есть функциональные тесты

Модульные тесты выполняются гораздо быстрее и лучше подходят для проверки граничных случаев. Кроме того, они не зависят от стороннего сервиса, поэтому вероятность ложноположительного результата ниже.

Как упростить код?

В идеале следует разбить код на небольшие функции. У каждой функции должно быть ограниченное число параметров и небольшое количество зависимостей от остальной части кода (слабая связанность):

  • Не передавайте функции большую структуру данных, если она использует только одно поле. Это делает входные данные функции (контракт) более понятными, а также снижает риск неожиданного преобразования структуры данных внутри функции.
  • Объект клиента boto сложен и может вызывать нежелательные побочные эффекты. Лучше изолировать вызовы в отдельных функциях. Для этих функций можно написать собственные модульные тесты.
  • Не передавайте объект module, если вам нужно лишь прочитать пару параметров из module.params. Передайте параметр непосредственно в функцию. Так вы явно укажете входные данные функции (контракт) и снизите вероятность побочных эффектов.

Рекомендации по модульным тестам

В идеале все module_utils должны покрываться модульными тестами. Однако мы понимаем, что написать модульные тесты может быть непросто, и принимаем изменения без них. В целом модульные тесты рекомендуются и, скорее всего, ускорят проверку PR.

  • Наши тесты запускаются с помощью pytest, и мы используем предоставляемые им возможности, например фикстуры и параметризацию.
  • Использовать unittest.TestCase не рекомендуется — ради единообразия и простоты.
  • Модульные тесты должны успешно выполняться без подключения к сети.
  • Необязательно имитировать все вызовы boto3/botocore (get_paginator(), paginate() и т. д.). Часто лучше создать функцию-обёртку для этих вызовов и имитировать её результат.
  • Главное — простота. Тесты должны быть короткими и охватывать ограниченный набор возможностей.

Документация Pytest подробна, примеры можно найти в руководствах с инструкциями

Как запустить модульные тесты

В нашей CI-системе тесты запускаются с помощью ansible-test. Локально их можно запустить следующей командой:

$ ansible-test units --docker

Мы также предоставляем конфигурацию tox, позволяющую быстрее запустить отдельный тест. В этом примере мы рассмотрим тесты для модуля s3_object:

$ tox -e py3 -- tests/unit/plugins/modules/test_s3_object.py

Форматирование кода

Для единообразия кода мы используем несколько средств форматирования и линтеров. Их можно запускать локально с помощью tox:

$ tox -m format
$ tox -m lint

Подробнее о каждом из используемых инструментов можно узнать на соответствующих сайтах:

  • black — инструмент форматирования кода с заданным стилем.
  • isort — группирует и сортирует импорты.
  • flynt — рекомендует использовать f-строки вместо таких альтернатив, как конкатенация, %, str.format() и string.Template.
  • flake8 — помогает следовать рекомендациям PEP8.
  • pylint — инструмент статического анализа кода.

© 2012–2018 Michael DeHaan
© 2018–2025 Red Hat, Inc.
Licensed under the GNU General Public License version 3.
https://docs.ansible.com/projects/ansible/latest/collections/amazon/aws/docsite/dev_guidelines.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API