Рекомендации по разработке модулей Ansible для Amazon AWS
Коллекция Ansible AWS (на Galaxy, репозиторий исходного кода) поддерживается рабочей группой Ansible AWS. Дополнительные сведения см. на странице сообщества рабочей группы AWS. Если вы планируете внести вклад в Ansible, добавив модули AWS, для начала стоит связаться с рабочей группой — в частности, потому, что аналогичный модуль может уже разрабатываться.
- Ограничение частоты запросов API и постраничная обработка
- Работа с JSON-политиками IAM
- Работа с тегами
- Форматирование кода
Требования
Совместимость с 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.
Информационные модули
Информационные модули, возвращающие сведения о нескольких ресурсах, должны возвращать список словарей, каждый из которых содержит сведения об отдельном ресурсе (например, 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
Вспомогательные функции
Помимо функций подключения в модуле 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 и преобразует их в хешируемый вид.
Этот метод следует использовать при любом сравнении политик, чтобы изменение порядка не приводило к ненужным изменениям.
Интеграционные тесты модулей 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_regionaws_access_keyaws_secret_keysecurity_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:
-
Создайте политику IAM, разрешающую все действия (задайте для
ActionиResourceзначение*). - Запустите тесты локально с этой политикой. Для модулей на основе AnsibleAWSModule параметр
debug_botocore_endpoint_logsавтоматически устанавливается в значениеyes, поэтому после PLAY RECAP вы должны увидеть список ACTIONS AWS со всеми использованными разрешениями. Если в тестах используется модуль boto/AnsibleModule, начните с наименее разрешительной политики (см. ниже). - Измените политику, разрешив только действия, используемые тестами. По возможности ограничьте учётную запись, регион и префикс. Подождите несколько минут, пока изменения политики вступят в силу.
- Запустите тесты ещё раз от имени пользователя или роли, которым разрешена только новая политика.
- Если тесты завершились с ошибкой, выясните её причину (см. советы ниже), измените политику, запустите тесты ещё раз и повторяйте процесс, пока тесты не пройдут с ограничительной политикой.
- Создайте запрос на слияние с предложением минимально необходимой политики для политик CI.
Чтобы начать с наименее разрешительной политики IAM:
- Запустите интеграционные тесты локально без разрешений IAM.
-
- Изучите ошибку, возникшую при сбое тестов.
-
- Если в сообщении об ошибке указано действие, использованное в запросе, добавьте это действие в политику.
-
- Если в сообщении об ошибке не указано действие, использованное в запросе:
-
- Обычно действие представляет собой название метода в CamelCase. Например, для клиента ec2 методу
describe_security_groupsсоответствует действиеec2:DescribeSecurityGroups. - Найдите нужное действие в документации.
- Обычно действие представляет собой название метода в CamelCase. Например, для клиента ec2 методу
- Если в сообщении об ошибке указан ARN ресурса, использованного в запросе, ограничьте действие этим ресурсом.
-
- Если в сообщении об ошибке не указан ARN ресурса:
-
- Изучите документацию и определите, можно ли ограничить действие конкретным ресурсом.
- Если действие можно ограничить, используйте документацию, чтобы сформировать ARN, и добавьте его в политику.
- Добавьте действие или ресурс, вызвавший сбой, в политику IAM. Подождите несколько минут, пока изменения политики вступят в силу.
- Запустите тесты ещё раз, назначив эту политику пользователю или роли.
- Если тесты снова завершаются с ошибкой на том же этапе и с тем же сообщением, необходимо выяснить причину (см. советы ниже). Если первый тест пройдёт, повторите шаги 2 и 3 для следующей ошибки. Продолжайте этот процесс, пока тесты не пройдут с ограничительной политикой.
- Создайте запрос на слияние с предложением минимально необходимой политики для политик 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