Repository: ansible-collections/amazon.aws

Issue:
[PR #958/47802269 backport][stable-4] Minor linting fixups - 2022-08-08
**This is a backport of PR #958 as merged into main (478022695b333043857a6929b350a2a3c07ae567).**

##### SUMMARY

Minor linting fixups

- docs
- unused variables

##### ISSUE TYPE

- Feature Pull Request

##### COMPONENT NAME

plugins/inventory/aws_ec2.py
plugins/inventory/aws_rds.py
plugins/lookup/aws_account_attribute.py
plugins/lookup/aws_secret.py
plugins/lookup/aws_ssm.py
plugins/module_utils/rds.py
tests/unit/plugins/inventory/test_aws_ec2.py

##### ADDITIONAL INFORMATION

Split from #953 due to integration test timeouts in community.aws


Discussion:

Resolving patch:
diff --git a/plugins/modules/iam_role.py b/plugins/modules/iam_role.py
new file mode 100644
--- /dev/null
+++ b/plugins/modules/iam_role.py
@@ -0,0 +1,772 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: Contributors to the Ansible project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r"""
+---
+module: iam_role
+version_added: 1.0.0
+version_added_collection: community.aws
+short_description: Manage AWS IAM roles
+description:
+  - Manage AWS IAM roles.
+author:
+  - "Rob White (@wimnat)"
+options:
+  path:
+    description:
+      - The path to the role. For more information about paths, see U(https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html).
+    default: "/"
+    type: str
+  name:
+    description:
+      - The name of the role to create.
+    required: true
+    type: str
+  description:
+    description:
+      - Provides a description of the role.
+    type: str
+  boundary:
+    description:
+      - The ARN of an IAM managed policy to use to restrict the permissions this role can pass on to IAM roles/users that it creates.
+      - Boundaries cannot be set on Instance Profiles, as such if this option is specified then I(create_instance_profile) must be C(false).
+      - This is intended for roles/users that have permissions to create new IAM objects.
+      - For more information on boundaries, see U(https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html).
+    aliases: [boundary_policy_arn]
+    type: str
+  assume_role_policy_document:
+    description:
+      - The trust relationship policy document that grants an entity permission to assume the role.
+      - This parameter is required when I(state=present).
+    type: json
+  managed_policies:
+    description:
+      - A list of managed policy ARNs, managed policy ARNs or friendly names.
+      - To remove all policies set I(purge_polices=true) and I(managed_policies=[None]).
+      - To embed an inline policy, use M(amazon.aws.iam_policy).
+    aliases: ['managed_policy']
+    type: list
+    elements: str
+  max_session_duration:
+    description:
+      - The maximum duration (in seconds) of a session when assuming the role.
+      - Valid values are between 1 and 12 hours (3600 and 43200 seconds).
+    type: int
+  purge_policies:
+    description:
+      - When I(purge_policies=true) any managed policies not listed in I(managed_policies) will be detatched.
+    type: bool
+    aliases: ['purge_policy', 'purge_managed_policies']
+    default: true
+  state:
+    description:
+      - Create or remove the IAM role.
+    default: present
+    choices: [ present, absent ]
+    type: str
+  create_instance_profile:
+    description:
+      - Creates an IAM instance profile along with the role.
+    default: true
+    type: bool
+  delete_instance_profile:
+    description:
+      - When I(delete_instance_profile=true) and I(state=absent) deleting a role will also delete the instance
+        profile created with the same I(name) as the role.
+      - Only applies when I(state=absent).
+    default: false
+    type: bool
+  wait_timeout:
+    description:
+      - How long (in seconds) to wait for creation / update to complete.
+    default: 120
+    type: int
+  wait:
+    description:
+      - When I(wait=True) the module will wait for up to I(wait_timeout) seconds
+        for IAM role creation before returning.
+    default: True
+    type: bool
+extends_documentation_fragment:
+  - amazon.aws.common.modules
+  - amazon.aws.region.modules
+  - amazon.aws.tags
+  - amazon.aws.boto3
+"""
+
+EXAMPLES = r"""
+# Note: These examples do not set authentication details, see the AWS Guide for details.
+
+- name: Create a role with description and tags
+  amazon.aws.iam_role:
+    name: mynewrole
+    assume_role_policy_document: "{{ lookup('file','policy.json') }}"
+    description: This is My New Role
+    tags:
+      env: dev
+
+- name: "Create a role and attach a managed policy called 'PowerUserAccess'"
+  amazon.aws.iam_role:
+    name: mynewrole
+    assume_role_policy_document: "{{ lookup('file','policy.json') }}"
+    managed_policies:
+      - arn:aws:iam::aws:policy/PowerUserAccess
+
+- name: Keep the role created above but remove all managed policies
+  amazon.aws.iam_role:
+    name: mynewrole
+    assume_role_policy_document: "{{ lookup('file','policy.json') }}"
+    managed_policies: []
+
+- name: Delete the role
+  amazon.aws.iam_role:
+    name: mynewrole
+    assume_role_policy_document: "{{ lookup('file', 'policy.json') }}"
+    state: absent
+
+"""
+RETURN = r"""
+iam_role:
+    description: dictionary containing the IAM Role data
+    returned: success
+    type: complex
+    contains:
+        path:
+            description: the path to the role
+            type: str
+            returned: always
+            sample: /
+        role_name:
+            description: the friendly name that identifies the role
+            type: str
+            returned: always
+            sample: myrole
+        role_id:
+            description: the stable and unique string identifying the role
+            type: str
+            returned: always
+            sample: ABCDEFF4EZ4ABCDEFV4ZC
+        arn:
+            description: the Amazon Resource Name (ARN) specifying the role
+            type: str
+            returned: always
+            sample: "arn:aws:iam::1234567890:role/mynewrole"
+        create_date:
+            description: the date and time, in ISO 8601 date-time format, when the role was created
+            type: str
+            returned: always
+            sample: "2016-08-14T04:36:28+00:00"
+        assume_role_policy_document:
+            description:
+              - the policy that grants an entity permission to assume the role
+              - |
+                note: the case of keys in this dictionary are currently converted from CamelCase to
+                snake_case.  In a release after 2023-12-01 this behaviour will change
+            type: dict
+            returned: always
+            sample: {
+                        'statement': [
+                            {
+                                'action': 'sts:AssumeRole',
+                                'effect': 'Allow',
+                                'principal': {
+                                    'service': 'ec2.amazonaws.com'
+                                },
+                                'sid': ''
+                            }
+                        ],
+                        'version': '2012-10-17'
+                    }
+        assume_role_policy_document_raw:
+            description: the policy that grants an entity permission to assume the role
+            type: dict
+            returned: always
+            version_added: 5.3.0
+            sample: {
+                        'Statement': [
+                            {
+                                'Action': 'sts:AssumeRole',
+                                'Effect': 'Allow',
+                                'Principal': {
+                                    'Service': 'ec2.amazonaws.com'
+                                },
+                                'Sid': ''
+                            }
+                        ],
+                        'Version': '2012-10-17'
+                    }
+
+        attached_policies:
+            description: a list of dicts containing the name and ARN of the managed IAM policies attached to the role
+            type: list
+            returned: always
+            sample: [
+                {
+                    'policy_arn': 'arn:aws:iam::aws:policy/PowerUserAccess',
+                    'policy_name': 'PowerUserAccess'
+                }
+            ]
+        tags:
+            description: role tags
+            type: dict
+            returned: always
+            sample: '{"Env": "Prod"}'
+"""
+
+import json
+
+try:
+    import botocore
+except ImportError:
+    pass  # caught by AnsibleAWSModule
+
+from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
+
+from ansible_collections.amazon.aws.plugins.module_utils.arn import validate_aws_arn
+from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code
+from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule
+from ansible_collections.amazon.aws.plugins.module_utils.policy import compare_policies
+from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry
+from ansible_collections.amazon.aws.plugins.module_utils.tagging import ansible_dict_to_boto3_tag_list
+from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict
+from ansible_collections.amazon.aws.plugins.module_utils.tagging import compare_aws_tags
+
+
+@AWSRetry.jittered_backoff()
+def _list_policies(client):
+    paginator = client.get_paginator("list_policies")
+    return paginator.paginate().build_full_result()["Policies"]
+
+
+def wait_iam_exists(module, client):
+    if module.check_mode:
+        return
+    if not module.params.get("wait"):
+        return
+
+    role_name = module.params.get("name")
+    wait_timeout = module.params.get("wait_timeout")
+
+    delay = min(wait_timeout, 5)
+    max_attempts = wait_timeout // delay
+
+    try:
+        waiter = client.get_waiter("role_exists")
+        waiter.wait(
+            WaiterConfig={"Delay": delay, "MaxAttempts": max_attempts},
+            RoleName=role_name,
+        )
+    except botocore.exceptions.WaiterError as e:
+        module.fail_json_aws(e, msg="Timeout while waiting on IAM role creation")
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg="Failed while waiting on IAM role creation")
+
+
+def convert_friendly_names_to_arns(module, client, policy_names):
+    if all(validate_aws_arn(policy, service="iam") for policy in policy_names if policy is not None):
+        return policy_names
+
+    allpolicies = {}
+    policies = _list_policies(client)
+
+    for policy in policies:
+        allpolicies[policy["PolicyName"]] = policy["Arn"]
+        allpolicies[policy["Arn"]] = policy["Arn"]
+    try:
+        return [allpolicies[policy] for policy in policy_names if policy is not None]
+    except KeyError as e:
+        module.fail_json_aws(e, msg="Couldn't find policy")
+
+
+def attach_policies(module, client, policies_to_attach, role_name):
+    if module.check_mode and policies_to_attach:
+        return True
+
+    changed = False
+    for policy_arn in policies_to_attach:
+        try:
+            client.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn, aws_retry=True)
+            changed = True
+        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+            module.fail_json_aws(e, msg=f"Unable to attach policy {policy_arn} to role {role_name}")
+    return changed
+
+
+def remove_policies(module, client, policies_to_remove, role_name):
+    if module.check_mode and policies_to_remove:
+        return True
+
+    changed = False
+    for policy in policies_to_remove:
+        try:
+            client.detach_role_policy(RoleName=role_name, PolicyArn=policy, aws_retry=True)
+            changed = True
+        except is_boto3_error_code("NoSuchEntityException"):
+            pass
+        except (
+            botocore.exceptions.ClientError,
+            botocore.exceptions.BotoCoreError,
+        ) as e:  # pylint: disable=duplicate-except
+            module.fail_json_aws(e, msg=f"Unable to detach policy {policy} from {role_name}")
+    return changed
+
+
+def remove_inline_policies(module, client, role_name):
+    current_inline_policies = get_inline_policy_list(module, client, role_name)
+    for policy in current_inline_policies:
+        try:
+            client.delete_role_policy(RoleName=role_name, PolicyName=policy, aws_retry=True)
+        except is_boto3_error_code("NoSuchEntityException"):
+            pass
+        except (
+            botocore.exceptions.ClientError,
+            botocore.exceptions.BotoCoreError,
+        ) as e:  # pylint: disable=duplicate-except
+            module.fail_json_aws(e, msg=f"Unable to delete policy {policy} embedded in {role_name}")
+
+
+def generate_create_params(module):
+    params = dict()
+    params["Path"] = module.params.get("path")
+    params["RoleName"] = module.params.get("name")
+    params["AssumeRolePolicyDocument"] = module.params.get("assume_role_policy_document")
+    if module.params.get("description") is not None:
+        params["Description"] = module.params.get("description")
+    if module.params.get("max_session_duration") is not None:
+        params["MaxSessionDuration"] = module.params.get("max_session_duration")
+    if module.params.get("boundary") is not None:
+        params["PermissionsBoundary"] = module.params.get("boundary")
+    if module.params.get("tags") is not None:
+        params["Tags"] = ansible_dict_to_boto3_tag_list(module.params.get("tags"))
+
+    return params
+
+
+def create_basic_role(module, client):
+    """
+    Perform the Role creation.
+    Assumes tests for the role existing have already been performed.
+    """
+    if module.check_mode:
+        module.exit_json(changed=True)
+
+    try:
+        params = generate_create_params(module)
+        role = client.create_role(aws_retry=True, **params)
+        # 'Description' is documented as key of the role returned by create_role
+        # but appears to be an AWS bug (the value is not returned using the AWS CLI either).
+        # Get the role after creating it.
+        role = get_role_with_backoff(module, client, params["RoleName"])
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg="Unable to create role")
+
+    return role
+
+
+def update_role_assumed_policy(module, client, role_name, target_assumed_policy, current_assumed_policy):
+    # Check Assumed Policy document
+    if target_assumed_policy is None or not compare_policies(current_assumed_policy, json.loads(target_assumed_policy)):
+        return False
+
+    if module.check_mode:
+        return True
+
+    try:
+        client.update_assume_role_policy(RoleName=role_name, PolicyDocument=target_assumed_policy, aws_retry=True)
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg=f"Unable to update assume role policy for role {role_name}")
+    return True
+
+
+def update_role_description(module, client, role_name, target_description, current_description):
+    # Check Description update
+    if target_description is None or current_description == target_description:
+        return False
+
+    if module.check_mode:
+        return True
+
+    try:
+        client.update_role(RoleName=role_name, Description=target_description, aws_retry=True)
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg=f"Unable to update description for role {role_name}")
+    return True
+
+
+def update_role_max_session_duration(module, client, role_name, target_duration, current_duration):
+    # Check MaxSessionDuration update
+    if target_duration is None or current_duration == target_duration:
+        return False
+
+    if module.check_mode:
+        return True
+
+    try:
+        client.update_role(RoleName=role_name, MaxSessionDuration=target_duration, aws_retry=True)
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg=f"Unable to update maximum session duration for role {role_name}")
+    return True
+
+
+def update_role_permissions_boundary(
+    module, client, role_name, target_permissions_boundary, current_permissions_boundary
+):
+    # Check PermissionsBoundary
+    if target_permissions_boundary is None or target_permissions_boundary == current_permissions_boundary:
+        return False
+
+    if module.check_mode:
+        return True
+
+    if target_permissions_boundary == "":
+        try:
+            client.delete_role_permissions_boundary(RoleName=role_name, aws_retry=True)
+        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+            module.fail_json_aws(e, msg=f"Unable to remove permission boundary for role {role_name}")
+    else:
+        try:
+            client.put_role_permissions_boundary(
+                RoleName=role_name, PermissionsBoundary=target_permissions_boundary, aws_retry=True
+            )
+        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+            module.fail_json_aws(e, msg=f"Unable to update permission boundary for role {role_name}")
+    return True
+
+
+def update_managed_policies(module, client, role_name, managed_policies, purge_policies):
+    # Check Managed Policies
+    if managed_policies is None:
+        return False
+
+    # Get list of current attached managed policies
+    current_attached_policies = get_attached_policy_list(module, client, role_name)
+    current_attached_policies_arn_list = [policy["PolicyArn"] for policy in current_attached_policies]
+
+    if len(managed_policies) == 1 and managed_policies[0] is None:
+        managed_policies = []
+
+    policies_to_remove = set(current_attached_policies_arn_list) - set(managed_policies)
+    policies_to_attach = set(managed_policies) - set(current_attached_policies_arn_list)
+
+    changed = False
+    if purge_policies and policies_to_remove:
+        if module.check_mode:
+            return True
+        else:
+            changed |= remove_policies(module, client, policies_to_remove, role_name)
+
+    if policies_to_attach:
+        if module.check_mode:
+            return True
+        else:
+            changed |= attach_policies(module, client, policies_to_attach, role_name)
+
+    return changed
+
+
+def create_or_update_role(module, client):
+    role_name = module.params.get("name")
+    assumed_policy = module.params.get("assume_role_policy_document")
+    create_instance_profile = module.params.get("create_instance_profile")
+    description = module.params.get("description")
+    duration = module.params.get("max_session_duration")
+    path = module.params.get("path")
+    permissions_boundary = module.params.get("boundary")
+    purge_tags = module.params.get("purge_tags")
+    tags = ansible_dict_to_boto3_tag_list(module.params.get("tags")) if module.params.get("tags") else None
+    purge_policies = module.params.get("purge_policies")
+    managed_policies = module.params.get("managed_policies")
+    if managed_policies:
+        # Attempt to list the policies early so we don't leave things behind if we can't find them.
+        managed_policies = convert_friendly_names_to_arns(module, client, managed_policies)
+
+    changed = False
+
+    # Get role
+    role = get_role(module, client, role_name)
+
+    # If role is None, create it
+    if role is None:
+        role = create_basic_role(module, client)
+
+        if not module.check_mode and module.params.get("wait"):
+            wait_iam_exists(module, client)
+
+        changed = True
+    else:
+        # Role exists - get current attributes
+        current_assumed_policy = role.get("AssumeRolePolicyDocument")
+        current_description = role.get("Description")
+        current_duration = role.get("MaxSessionDuration")
+        current_permissions_boundary = role.get("PermissionsBoundary", {}).get("PermissionsBoundaryArn", "")
+
+        # Update attributes
+        changed |= update_role_tags(module, client, role_name, tags, purge_tags)
+        changed |= update_role_assumed_policy(module, client, role_name, assumed_policy, current_assumed_policy)
+        changed |= update_role_description(module, client, role_name, description, current_description)
+        changed |= update_role_max_session_duration(module, client, role_name, duration, current_duration)
+        changed |= update_role_permissions_boundary(
+            module, client, role_name, permissions_boundary, current_permissions_boundary
+        )
+
+        if not module.check_mode and module.params.get("wait"):
+            wait_iam_exists(module, client)
+
+    if create_instance_profile:
+        changed |= create_instance_profiles(module, client, role_name, path)
+
+        if not module.check_mode and module.params.get("wait"):
+            wait_iam_exists(module, client)
+
+    changed |= update_managed_policies(module, client, role_name, managed_policies, purge_policies)
+    wait_iam_exists(module, client)
+
+    # Get the role again
+    role = get_role(module, client, role_name)
+    role["AttachedPolicies"] = get_attached_policy_list(module, client, role_name)
+    role["tags"] = get_role_tags(module, client)
+
+    camel_role = camel_dict_to_snake_dict(role, ignore_list=["tags"])
+    camel_role["assume_role_policy_document_raw"] = role.get("AssumeRolePolicyDocument", {})
+    module.exit_json(changed=changed, iam_role=camel_role, **camel_role)
+
+
+def create_instance_profiles(module, client, role_name, path):
+    # Fetch existing Profiles
+    try:
+        instance_profiles = client.list_instance_profiles_for_role(RoleName=role_name, aws_retry=True)[
+            "InstanceProfiles"
+        ]
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg=f"Unable to list instance profiles for role {role_name}")
+
+    # Profile already exists
+    if any(p["InstanceProfileName"] == role_name for p in instance_profiles):
+        return False
+
+    if module.check_mode:
+        return True
+
+    # Make sure an instance profile is created
+    try:
+        client.create_instance_profile(InstanceProfileName=role_name, Path=path, aws_retry=True)
+    except is_boto3_error_code("EntityAlreadyExists"):
+        # If the profile already exists, no problem, move on.
+        # Implies someone's changing things at the same time...
+        return False
+    except (
+        botocore.exceptions.ClientError,
+        botocore.exceptions.BotoCoreError,
+    ) as e:  # pylint: disable=duplicate-except
+        module.fail_json_aws(e, msg=f"Unable to create instance profile for role {role_name}")
+
+    # And attach the role to the profile
+    try:
+        client.add_role_to_instance_profile(InstanceProfileName=role_name, RoleName=role_name, aws_retry=True)
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg=f"Unable to attach role {role_name} to instance profile {role_name}")
+
+    return True
+
+
+def remove_instance_profiles(module, client, role_name):
+    delete_profiles = module.params.get("delete_instance_profile")
+
+    try:
+        instance_profiles = client.list_instance_profiles_for_role(aws_retry=True, RoleName=role_name)[
+            "InstanceProfiles"
+        ]
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg=f"Unable to list instance profiles for role {role_name}")
+
+    # Remove the role from the instance profile(s)
+    for profile in instance_profiles:
+        profile_name = profile["InstanceProfileName"]
+        try:
+            if not module.check_mode:
+                client.remove_role_from_instance_profile(
+                    aws_retry=True, InstanceProfileName=profile_name, RoleName=role_name
+                )
+                if profile_name == role_name:
+                    if delete_profiles:
+                        try:
+                            client.delete_instance_profile(InstanceProfileName=profile_name, aws_retry=True)
+                        except is_boto3_error_code("NoSuchEntityException"):
+                            pass
+                        except (
+                            botocore.exceptions.ClientError,
+                            botocore.exceptions.BotoCoreError,
+                        ) as e:  # pylint: disable=duplicate-except
+                            module.fail_json_aws(e, msg=f"Unable to remove instance profile {profile_name}")
+        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+            module.fail_json_aws(e, msg=f"Unable to remove role {role_name} from instance profile {profile_name}")
+
+
+def destroy_role(module, client):
+    role_name = module.params.get("name")
+    role = get_role(module, client, role_name)
+
+    if role is None:
+        module.exit_json(changed=False)
+
+    if not module.check_mode:
+        # Before we try to delete the role we need to remove any
+        # - attached instance profiles
+        # - attached managed policies
+        # - embedded inline policies
+        remove_instance_profiles(module, client, role_name)
+        update_managed_policies(module, client, role_name, [], True)
+        remove_inline_policies(module, client, role_name)
+        try:
+            client.delete_role(aws_retry=True, RoleName=role_name)
+        except is_boto3_error_code("NoSuchEntityException"):
+            module.exit_json(changed=False)
+        except (
+            botocore.exceptions.ClientError,
+            botocore.exceptions.BotoCoreError,
+        ) as e:  # pylint: disable=duplicate-except
+            module.fail_json_aws(e, msg="Unable to delete role")
+
+    module.exit_json(changed=True)
+
+
+def get_role_with_backoff(module, client, name):
+    try:
+        return AWSRetry.jittered_backoff(catch_extra_error_codes=["NoSuchEntity"])(client.get_role)(RoleName=name)[
+            "Role"
+        ]
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg=f"Unable to get role {name}")
+
+
+def get_role(module, client, name):
+    try:
+        return client.get_role(RoleName=name, aws_retry=True)["Role"]
+    except is_boto3_error_code("NoSuchEntity"):
+        return None
+    except (
+        botocore.exceptions.ClientError,
+        botocore.exceptions.BotoCoreError,
+    ) as e:  # pylint: disable=duplicate-except
+        module.fail_json_aws(e, msg=f"Unable to get role {name}")
+
+
+def get_attached_policy_list(module, client, name):
+    try:
+        return client.list_attached_role_policies(RoleName=name, aws_retry=True)["AttachedPolicies"]
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg=f"Unable to list attached policies for role {name}")
+
+
+def get_inline_policy_list(module, client, name):
+    try:
+        return client.list_role_policies(RoleName=name, aws_retry=True)["PolicyNames"]
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg=f"Unable to list attached policies for role {name}")
+
+
+def get_role_tags(module, client):
+    role_name = module.params.get("name")
+    try:
+        return boto3_tag_list_to_ansible_dict(client.list_role_tags(RoleName=role_name, aws_retry=True)["Tags"])
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg=f"Unable to list tags for role {role_name}")
+
+
+def update_role_tags(module, client, role_name, new_tags, purge_tags):
+    if new_tags is None:
+        return False
+    new_tags = boto3_tag_list_to_ansible_dict(new_tags)
+
+    try:
+        existing_tags = boto3_tag_list_to_ansible_dict(
+            client.list_role_tags(RoleName=role_name, aws_retry=True)["Tags"]
+        )
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError, KeyError):
+        existing_tags = {}
+
+    tags_to_add, tags_to_remove = compare_aws_tags(existing_tags, new_tags, purge_tags=purge_tags)
+
+    if not module.check_mode:
+        try:
+            if tags_to_remove:
+                client.untag_role(RoleName=role_name, TagKeys=tags_to_remove, aws_retry=True)
+            if tags_to_add:
+                client.tag_role(RoleName=role_name, Tags=ansible_dict_to_boto3_tag_list(tags_to_add), aws_retry=True)
+        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+            module.fail_json_aws(e, msg=f"Unable to set tags for role {role_name}")
+
+    changed = bool(tags_to_add) or bool(tags_to_remove)
+    return changed
+
+
+def main():
+    argument_spec = dict(
+        name=dict(type="str", required=True),
+        path=dict(type="str", default="/"),
+        assume_role_policy_document=dict(type="json"),
+        managed_policies=dict(type="list", aliases=["managed_policy"], elements="str"),
+        max_session_duration=dict(type="int"),
+        state=dict(type="str", choices=["present", "absent"], default="present"),
+        description=dict(type="str"),
+        boundary=dict(type="str", aliases=["boundary_policy_arn"]),
+        create_instance_profile=dict(type="bool", default=True),
+        delete_instance_profile=dict(type="bool", default=False),
+        purge_policies=dict(default=True, type="bool", aliases=["purge_policy", "purge_managed_policies"]),
+        tags=dict(type="dict", aliases=["resource_tags"]),
+        purge_tags=dict(type="bool", default=True),
+        wait=dict(type="bool", default=True),
+        wait_timeout=dict(default=120, type="int"),
+    )
+
+    module = AnsibleAWSModule(
+        argument_spec=argument_spec,
+        required_if=[("state", "present", ["assume_role_policy_document"])],
+        supports_check_mode=True,
+    )
+
+    module.deprecate(
+        "All return values other than iam_role and changed have been deprecated and "
+        "will be removed in a release after 2023-12-01.",
+        date="2023-12-01",
+        collection_name="amazon.aws",
+    )
+
+    module.deprecate(
+        "In a release after 2023-12-01 the contents of iam_role.assume_role_policy_document "
+        "will no longer be converted from CamelCase to snake_case.  The "
+        "iam_role.assume_role_policy_document_raw return value already returns the "
+        "policy document in this future format.",
+        date="2023-12-01",
+        collection_name="amazon.aws",
+    )
+
+    if module.params.get("boundary"):
+        if module.params.get("create_instance_profile"):
+            module.fail_json(msg="When using a boundary policy, `create_instance_profile` must be set to `false`.")
+        if not validate_aws_arn(module.params.get("boundary"), service="iam"):
+            module.fail_json(msg="Boundary policy must be an ARN")
+    if module.params.get("max_session_duration"):
+        max_session_duration = module.params.get("max_session_duration")
+        if max_session_duration < 3600 or max_session_duration > 43200:
+            module.fail_json(msg="max_session_duration must be between 1 and 12 hours (3600 and 43200 seconds)")
+    if module.params.get("path"):
+        path = module.params.get("path")
+        if not path.endswith("/") or not path.startswith("/"):
+            module.fail_json(msg="path must begin and end with /")
+
+    client = module.client("iam", retry_decorator=AWSRetry.jittered_backoff())
+
+    state = module.params.get("state")
+
+    if state == "present":
+        create_or_update_role(module, client)
+    elif state == "absent":
+        destroy_role(module, client)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/plugins/modules/iam_role_info.py b/plugins/modules/iam_role_info.py
new file mode 100644
--- /dev/null
+++ b/plugins/modules/iam_role_info.py
@@ -0,0 +1,288 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r"""
+---
+module: iam_role_info
+version_added: 1.0.0
+version_added_collection: community.aws
+short_description: Gather information on IAM roles
+description:
+    - Gathers information about IAM roles.
+author:
+    - "Will Thames (@willthames)"
+options:
+    name:
+        description:
+            - Name of a role to search for.
+            - Mutually exclusive with I(path_prefix).
+        aliases:
+            - role_name
+        type: str
+    path_prefix:
+        description:
+            - Prefix of role to restrict IAM role search for.
+            - Mutually exclusive with I(name).
+        type: str
+extends_documentation_fragment:
+    - amazon.aws.common.modules
+    - amazon.aws.region.modules
+    - amazon.aws.boto3
+"""
+
+EXAMPLES = r"""
+- name: find all existing IAM roles
+  amazon.aws.iam_role_info:
+  register: result
+
+- name: describe a single role
+  amazon.aws.iam_role_info:
+    name: MyIAMRole
+
+- name: describe all roles matching a path prefix
+  amazon.aws.iam_role_info:
+    path_prefix: /application/path
+"""
+
+RETURN = r"""
+iam_roles:
+  description: List of IAM roles
+  returned: always
+  type: complex
+  contains:
+    arn:
+      description: Amazon Resource Name for IAM role.
+      returned: always
+      type: str
+      sample: arn:aws:iam::123456789012:role/AnsibleTestRole
+    assume_role_policy_document:
+      description:
+        - The policy that grants an entity permission to assume the role
+        - |
+          Note: the case of keys in this dictionary are currently converted from CamelCase to
+          snake_case.  In a release after 2023-12-01 this behaviour will change.
+      returned: always
+      type: dict
+    assume_role_policy_document_raw:
+      description: The policy document describing what can assume the role.
+      returned: always
+      type: dict
+      version_added: 5.3.0
+    create_date:
+      description: Date IAM role was created.
+      returned: always
+      type: str
+      sample: '2017-10-23T00:05:08+00:00'
+    inline_policies:
+      description: List of names of inline policies.
+      returned: always
+      type: list
+      sample: []
+    managed_policies:
+      description: List of attached managed policies.
+      returned: always
+      type: complex
+      contains:
+        policy_arn:
+          description: Amazon Resource Name for the policy.
+          returned: always
+          type: str
+          sample: arn:aws:iam::123456789012:policy/AnsibleTestEC2Policy
+        policy_name:
+          description: Name of managed policy.
+          returned: always
+          type: str
+          sample: AnsibleTestEC2Policy
+    instance_profiles:
+      description: List of attached instance profiles.
+      returned: always
+      type: complex
+      contains:
+        arn:
+          description: Amazon Resource Name for the instance profile.
+          returned: always
+          type: str
+          sample: arn:aws:iam::123456789012:instance-profile/AnsibleTestEC2Policy
+        create_date:
+          description: Date instance profile was created.
+          returned: always
+          type: str
+          sample: '2017-10-23T00:05:08+00:00'
+        instance_profile_id:
+          description: Amazon Identifier for the instance profile.
+          returned: always
+          type: str
+          sample: AROAII7ABCD123456EFGH
+        instance_profile_name:
+          description: Name of instance profile.
+          returned: always
+          type: str
+          sample: AnsibleTestEC2Policy
+        path:
+          description: Path of instance profile.
+          returned: always
+          type: str
+          sample: /
+        roles:
+          description: List of roles associated with this instance profile.
+          returned: always
+          type: list
+          sample: []
+    path:
+      description: Path of role.
+      returned: always
+      type: str
+      sample: /
+    role_id:
+      description: Amazon Identifier for the role.
+      returned: always
+      type: str
+      sample: AROAII7ABCD123456EFGH
+    role_name:
+      description: Name of the role.
+      returned: always
+      type: str
+      sample: AnsibleTestRole
+    tags:
+      description: Role tags.
+      type: dict
+      returned: always
+      sample: '{"Env": "Prod"}'
+"""
+
+try:
+    import botocore
+except ImportError:
+    pass  # caught by AnsibleAWSModule
+
+from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
+
+from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code
+from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule
+from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry
+from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict
+
+
+@AWSRetry.jittered_backoff()
+def list_iam_roles_with_backoff(client, **kwargs):
+    paginator = client.get_paginator("list_roles")
+    return paginator.paginate(**kwargs).build_full_result()
+
+
+@AWSRetry.jittered_backoff()
+def list_iam_role_policies_with_backoff(client, role_name):
+    paginator = client.get_paginator("list_role_policies")
+    return paginator.paginate(RoleName=role_name).build_full_result()["PolicyNames"]
+
+
+@AWSRetry.jittered_backoff()
+def list_iam_attached_role_policies_with_backoff(client, role_name):
+    paginator = client.get_paginator("list_attached_role_policies")
+    return paginator.paginate(RoleName=role_name).build_full_result()["AttachedPolicies"]
+
+
+@AWSRetry.jittered_backoff()
+def list_iam_instance_profiles_for_role_with_backoff(client, role_name):
+    paginator = client.get_paginator("list_instance_profiles_for_role")
+    return paginator.paginate(RoleName=role_name).build_full_result()["InstanceProfiles"]
+
+
+def describe_iam_role(module, client, role):
+    name = role["RoleName"]
+    try:
+        role["InlinePolicies"] = list_iam_role_policies_with_backoff(client, name)
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg=f"Couldn't get inline policies for role {name}")
+    try:
+        role["ManagedPolicies"] = list_iam_attached_role_policies_with_backoff(client, name)
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg=f"Couldn't get managed  policies for role {name}")
+    try:
+        role["InstanceProfiles"] = list_iam_instance_profiles_for_role_with_backoff(client, name)
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg=f"Couldn't get instance profiles for role {name}")
+    try:
+        role["tags"] = boto3_tag_list_to_ansible_dict(role["Tags"])
+        del role["Tags"]
+    except KeyError:
+        role["tags"] = {}
+    return role
+
+
+def describe_iam_roles(module, client):
+    name = module.params["name"]
+    path_prefix = module.params["path_prefix"]
+    if name:
+        try:
+            roles = [client.get_role(RoleName=name, aws_retry=True)["Role"]]
+        except is_boto3_error_code("NoSuchEntity"):
+            return []
+        except (
+            botocore.exceptions.ClientError,
+            botocore.exceptions.BotoCoreError,
+        ) as e:  # pylint: disable=duplicate-except
+            module.fail_json_aws(e, msg=f"Couldn't get IAM role {name}")
+    else:
+        params = dict()
+        if path_prefix:
+            if not path_prefix.startswith("/"):
+                path_prefix = "/" + path_prefix
+            if not path_prefix.endswith("/"):
+                path_prefix = path_prefix + "/"
+            params["PathPrefix"] = path_prefix
+        try:
+            roles = list_iam_roles_with_backoff(client, **params)["Roles"]
+        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+            module.fail_json_aws(e, msg="Couldn't list IAM roles")
+    return [normalize_role(describe_iam_role(module, client, role)) for role in roles]
+
+
+def normalize_profile(profile):
+    new_profile = camel_dict_to_snake_dict(profile)
+    if profile.get("Roles"):
+        profile["roles"] = [normalize_role(role) for role in profile.get("Roles")]
+    return new_profile
+
+
+def normalize_role(role):
+    new_role = camel_dict_to_snake_dict(role, ignore_list=["tags"])
+    new_role["assume_role_policy_document_raw"] = role.get("AssumeRolePolicyDocument")
+    if role.get("InstanceProfiles"):
+        role["instance_profiles"] = [normalize_profile(profile) for profile in role.get("InstanceProfiles")]
+    return new_role
+
+
+def main():
+    """
+    Module action handler
+    """
+    argument_spec = dict(
+        name=dict(aliases=["role_name"]),
+        path_prefix=dict(),
+    )
+
+    module = AnsibleAWSModule(
+        argument_spec=argument_spec,
+        supports_check_mode=True,
+        mutually_exclusive=[["name", "path_prefix"]],
+    )
+
+    client = module.client("iam", retry_decorator=AWSRetry.jittered_backoff())
+
+    module.deprecate(
+        "In a release after 2023-12-01 the contents of assume_role_policy_document "
+        "will no longer be converted from CamelCase to snake_case.  The "
+        ".assume_role_policy_document_raw return value already returns the "
+        "policy document in this future format.",
+        date="2023-12-01",
+        collection_name="amazon.aws",
+    )
+
+    module.exit_json(changed=False, iam_roles=describe_iam_roles(module, client))
+
+
+if __name__ == "__main__":
+    main()


Test patch:
diff --git a/tests/integration/targets/iam_role/aliases b/tests/integration/targets/iam_role/aliases
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/aliases
@@ -0,0 +1,9 @@
+# reason: missing-policy
+# It should be possible to test iam_role by limiting which policies can be
+# attached to the roles.
+# Careful review is needed prior to adding this to the main CI.
+unsupported
+
+cloud/aws
+
+iam_role_info
diff --git a/tests/integration/targets/iam_role/defaults/main.yml b/tests/integration/targets/iam_role/defaults/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/defaults/main.yml
@@ -0,0 +1,5 @@
+test_role: '{{ resource_prefix }}-role'
+test_path: /{{ resource_prefix }}/
+safe_managed_policy: AWSDenyAll
+custom_policy_name: '{{ resource_prefix }}-denyall'
+boundary_policy: arn:aws:iam::aws:policy/AWSDenyAll
diff --git a/tests/integration/targets/iam_role/files/deny-all-a.json b/tests/integration/targets/iam_role/files/deny-all-a.json
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/files/deny-all-a.json
@@ -0,0 +1,13 @@
+{
+    "Version": "2012-10-17",
+    "Statement": [
+        {
+            "Action": [
+                "*"
+            ],
+            "Effect": "Deny",
+            "Resource": "*",
+            "Sid": "DenyA"
+        }
+    ]
+}
diff --git a/tests/integration/targets/iam_role/files/deny-all-b.json b/tests/integration/targets/iam_role/files/deny-all-b.json
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/files/deny-all-b.json
@@ -0,0 +1,13 @@
+{
+    "Version": "2012-10-17",
+    "Statement": [
+        {
+            "Action": [
+                "*"
+            ],
+            "Effect": "Deny",
+            "Resource": "*",
+            "Sid": "DenyB"
+        }
+    ]
+}
diff --git a/tests/integration/targets/iam_role/files/deny-all.json b/tests/integration/targets/iam_role/files/deny-all.json
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/files/deny-all.json
@@ -0,0 +1,12 @@
+{
+    "Version": "2012-10-17",
+    "Statement": [
+        {
+            "Action": [
+                "*"
+            ],
+            "Effect": "Deny",
+            "Resource": "*"
+        }
+    ]
+}
diff --git a/tests/integration/targets/iam_role/files/deny-assume.json b/tests/integration/targets/iam_role/files/deny-assume.json
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/files/deny-assume.json
@@ -0,0 +1,10 @@
+{
+    "Version": "2012-10-17",
+    "Statement": [
+        {
+            "Action": "sts:AssumeRole",
+            "Principal": { "Service": "ec2.amazonaws.com" },
+            "Effect": "Deny"
+        }
+    ]
+}
diff --git a/tests/integration/targets/iam_role/meta/main.yml b/tests/integration/targets/iam_role/meta/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/meta/main.yml
@@ -0,0 +1 @@
+dependencies: []
diff --git a/tests/integration/targets/iam_role/tasks/boundary_policy.yml b/tests/integration/targets/iam_role/tasks/boundary_policy.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/tasks/boundary_policy.yml
@@ -0,0 +1,86 @@
+- name: Create minimal role with no boundary policy
+  iam_role:
+    name: '{{ test_role }}'
+    create_instance_profile: no
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+
+- name: Configure Boundary Policy (CHECK MODE)
+  iam_role:
+    name: '{{ test_role }}'
+    create_instance_profile: no
+    boundary: '{{ boundary_policy }}'
+  check_mode: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: Configure Boundary Policy
+  iam_role:
+    name: '{{ test_role }}'
+    create_instance_profile: no
+    boundary: '{{ boundary_policy }}'
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+
+- name: Configure Boundary Policy (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    create_instance_profile: no
+    boundary: '{{ boundary_policy }}'
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Configure Boundary Policy (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    create_instance_profile: no
+    boundary: '{{ boundary_policy }}'
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+    - iam_role.iam_role.role_name == test_role
+
+- name: iam_role_info after adding boundary policy
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - '"description" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 0
+    - role_info.iam_roles[0].managed_policies | length == 0
+    - role_info.iam_roles[0].max_session_duration == 3600
+    - role_info.iam_roles[0].path == '/'
+    - role_info.iam_roles[0].permissions_boundary.permissions_boundary_arn == boundary_policy
+    - role_info.iam_roles[0].permissions_boundary.permissions_boundary_type == 'Policy'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+
+- name: Remove IAM Role
+  iam_role:
+    state: absent
+    name: '{{ test_role }}'
+    delete_instance_profile: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
diff --git a/tests/integration/targets/iam_role/tasks/complex_role_creation.yml b/tests/integration/targets/iam_role/tasks/complex_role_creation.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/tasks/complex_role_creation.yml
@@ -0,0 +1,128 @@
+- name: Complex IAM Role (CHECK MODE)
+  iam_role:
+    name: '{{ test_role }}'
+    assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}'
+    boundary: '{{ boundary_policy }}'
+    create_instance_profile: no
+    description: Ansible Test Role {{ resource_prefix }}
+    managed_policy:
+    - '{{ safe_managed_policy }}'
+    - '{{ custom_policy_name }}'
+    max_session_duration: 43200
+    path: '{{ test_path }}'
+    tags:
+      TagA: ValueA
+  check_mode: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: iam_role_info after Complex Role creation in check_mode
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 0
+
+- name: Complex IAM Role
+  iam_role:
+    name: '{{ test_role }}'
+    assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}'
+    boundary: '{{ boundary_policy }}'
+    create_instance_profile: no
+    description: Ansible Test Role {{ resource_prefix }}
+    managed_policy:
+    - '{{ safe_managed_policy }}'
+    - '{{ custom_policy_name }}'
+    max_session_duration: 43200
+    path: '{{ test_path }}'
+    tags:
+      TagA: ValueA
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+    - iam_role.iam_role.arn.startswith("arn")
+    - iam_role.iam_role.arn.endswith("role" + test_path + test_role )
+      # Would be nice to test the contents...
+    - '"assume_role_policy_document" in iam_role.iam_role'
+    - iam_role.iam_role.attached_policies | length == 2
+    - iam_role.iam_role.max_session_duration == 43200
+    - iam_role.iam_role.path == test_path
+    - iam_role.iam_role.role_name == test_role
+    - '"create_date" in iam_role.iam_role'
+    - '"role_id" in iam_role.iam_role'
+
+- name: Complex IAM role (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}'
+    boundary: '{{ boundary_policy }}'
+    create_instance_profile: no
+    description: Ansible Test Role {{ resource_prefix }}
+    managed_policy:
+    - '{{ safe_managed_policy }}'
+    - '{{ custom_policy_name }}'
+    max_session_duration: 43200
+    path: '{{ test_path }}'
+    tags:
+      TagA: ValueA
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Complex IAM role (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}'
+    boundary: '{{ boundary_policy }}'
+    create_instance_profile: no
+    description: Ansible Test Role {{ resource_prefix }}
+    managed_policy:
+    - '{{ safe_managed_policy }}'
+    - '{{ custom_policy_name }}'
+    max_session_duration: 43200
+    path: '{{ test_path }}'
+    tags:
+      TagA: ValueA
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+    - iam_role.iam_role.role_name == test_role
+
+- name: iam_role_info after Role creation
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role" + test_path + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].description == "Ansible Test Role {{ resource_prefix
+      }}"
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 0
+    - role_info.iam_roles[0].managed_policies | length == 2
+    - safe_managed_policy in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name")
+      | list | flatten )
+    - custom_policy_name in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name")
+      | list | flatten )
+    - role_info.iam_roles[0].max_session_duration == 43200
+    - role_info.iam_roles[0].path == test_path
+    - role_info.iam_roles[0].permissions_boundary.permissions_boundary_arn == boundary_policy
+    - role_info.iam_roles[0].permissions_boundary.permissions_boundary_type == 'Policy'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - '"TagA" in role_info.iam_roles[0].tags'
+    - role_info.iam_roles[0].tags.TagA == "ValueA"
diff --git a/tests/integration/targets/iam_role/tasks/creation_deletion.yml b/tests/integration/targets/iam_role/tasks/creation_deletion.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/tasks/creation_deletion.yml
@@ -0,0 +1,376 @@
+- name: Try running some rapid fire create/delete tests
+  block:
+  - name: Minimal IAM Role without instance profile (rapid)
+    iam_role:
+      name: '{{ test_role }}'
+      create_instance_profile: no
+    register: iam_role
+  - name: Minimal IAM Role without instance profile (rapid)
+    iam_role:
+      name: '{{ test_role }}'
+      create_instance_profile: no
+    register: iam_role_again
+  - assert:
+      that:
+      - iam_role is changed
+      - iam_role_again is not changed
+
+  - name: Remove IAM Role (rapid)
+    iam_role:
+      state: absent
+      name: '{{ test_role }}'
+    register: iam_role
+  - name: Remove IAM Role (rapid)
+    iam_role:
+      state: absent
+      name: '{{ test_role }}'
+    register: iam_role_again
+  - assert:
+      that:
+      - iam_role is changed
+      - iam_role_again is not changed
+
+  - name: Minimal IAM Role without instance profile (rapid)
+    iam_role:
+      name: '{{ test_role }}'
+      create_instance_profile: no
+    register: iam_role
+  - name: Remove IAM Role (rapid)
+    iam_role:
+      state: absent
+      name: '{{ test_role }}'
+    register: iam_role_again
+  - assert:
+      that:
+      - iam_role is changed
+      - iam_role_again is changed
+
+# ===================================================================
+# Role Creation
+# (without Instance profile)
+- name: iam_role_info before Role creation (no args)
+  iam_role_info:
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+
+- name: iam_role_info before Role creation (search for test role)
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 0
+
+- name: Minimal IAM Role (CHECK MODE)
+  iam_role:
+    name: '{{ test_role }}'
+    create_instance_profile: no
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is changed
+
+- name: iam_role_info after Role creation in check_mode
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 0
+
+- name: Minimal IAM Role without instance profile
+  iam_role:
+    name: '{{ test_role }}'
+    create_instance_profile: no
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+    - iam_role.iam_role.arn.startswith("arn")
+    - iam_role.iam_role.arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in iam_role.iam_role'
+    - '"assume_role_policy_document_raw" in iam_role.iam_role'
+    - iam_role.iam_role.assume_role_policy_document_raw == assume_deny_policy
+    - iam_role.iam_role.attached_policies | length == 0
+    - iam_role.iam_role.max_session_duration == 3600
+    - iam_role.iam_role.path == '/'
+    - iam_role.iam_role.role_name == test_role
+    - '"create_date" in iam_role.iam_role'
+    - '"role_id" in iam_role.iam_role'
+
+- name: Minimal IAM Role without instance profile (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    create_instance_profile: no
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Minimal IAM Role without instance profile (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    create_instance_profile: no
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+    - iam_role.iam_role.role_name == test_role
+
+- name: iam_role_info after Role creation
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"assume_role_policy_document_raw" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - '"description" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].assume_role_policy_document_raw == assume_deny_policy
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 0
+    - role_info.iam_roles[0].managed_policies | length == 0
+    - role_info.iam_roles[0].max_session_duration == 3600
+    - role_info.iam_roles[0].path == '/'
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 0
+
+- name: Remove IAM Role
+  iam_role:
+    state: absent
+    name: '{{ test_role }}'
+    delete_instance_profile: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: iam_role_info after Role deletion
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 0
+
+# ------------------------------------------------------------------------------------------
+
+# (with path)
+- name: Minimal IAM Role with path (CHECK MODE)
+  iam_role:
+    name: '{{ test_role }}'
+    path: '{{ test_path }}'
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is changed
+
+- name: Minimal IAM Role with path
+  iam_role:
+    name: '{{ test_role }}'
+    path: '{{ test_path }}'
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+    - iam_role.iam_role.arn.startswith("arn")
+    - iam_role.iam_role.arn.endswith("role" + test_path + test_role )
+      # Would be nice to test the contents...
+    - '"assume_role_policy_document" in iam_role.iam_role'
+    - iam_role.iam_role.attached_policies | length == 0
+    - iam_role.iam_role.max_session_duration == 3600
+    - iam_role.iam_role.path == '{{ test_path }}'
+    - iam_role.iam_role.role_name == test_role
+    - '"create_date" in iam_role.iam_role'
+    - '"role_id" in iam_role.iam_role'
+
+- name: Minimal IAM Role with path (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    path: '{{ test_path }}'
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Minimal IAM Role with path (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    path: '{{ test_path }}'
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+    - iam_role.iam_role.role_name == test_role
+
+- name: iam_role_info after Role creation
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role" + test_path + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - '"description" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 1
+    - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role
+    - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile"
+      + test_path + test_role)
+    - role_info.iam_roles[0].managed_policies | length == 0
+    - role_info.iam_roles[0].max_session_duration == 3600
+    - role_info.iam_roles[0].path == '{{ test_path }}'
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 0
+
+- name: iam_role_info after Role creation (searching a path)
+  iam_role_info:
+    path_prefix: '{{ test_path }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role" + test_path + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - '"description" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 1
+    - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role
+    - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile"
+      + test_path + test_role)
+    - role_info.iam_roles[0].managed_policies | length == 0
+    - role_info.iam_roles[0].max_session_duration == 3600
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].path == '{{ test_path }}'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 0
+
+- name: Remove IAM Role
+  iam_role:
+    state: absent
+    name: '{{ test_role }}'
+    path: '{{ test_path }}'
+    delete_instance_profile: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: iam_role_info after Role deletion
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 0
+
+# ------------------------------------------------------------------------------------------
+
+# (with Instance profile)
+- name: Minimal IAM Role with instance profile - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    create_instance_profile: yes
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is changed
+
+- name: Minimal IAM Role with instance profile
+  iam_role:
+    name: '{{ test_role }}'
+    create_instance_profile: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+    - iam_role.iam_role.arn.startswith("arn")
+    - iam_role.iam_role.arn.endswith("role/" + test_role )
+      # Would be nice to test the contents...
+    - '"assume_role_policy_document" in iam_role.iam_role'
+    - iam_role.iam_role.attached_policies | length == 0
+    - iam_role.iam_role.max_session_duration == 3600
+    - iam_role.iam_role.path == '/'
+    - iam_role.iam_role.role_name == test_role
+    - '"create_date" in iam_role.iam_role'
+    - '"role_id" in iam_role.iam_role'
+
+- name: Minimal IAM Role wth instance profile (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    create_instance_profile: yes
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Minimal IAM Role wth instance profile (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    create_instance_profile: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+    - iam_role.iam_role.role_name == test_role
+
+- name: iam_role_info after Role creation
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - '"description" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 1
+    - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role
+    - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/"
+      + test_role)
+    - role_info.iam_roles[0].managed_policies | length == 0
+    - role_info.iam_roles[0].max_session_duration == 3600
+    - role_info.iam_roles[0].path == '/'
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 0
diff --git a/tests/integration/targets/iam_role/tasks/description_update.yml b/tests/integration/targets/iam_role/tasks/description_update.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/tasks/description_update.yml
@@ -0,0 +1,143 @@
+- name: Add Description (CHECK MODE)
+  iam_role:
+    name: '{{ test_role }}'
+    description: Ansible Test Role {{ resource_prefix }}
+  check_mode: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: Add Description
+  iam_role:
+    name: '{{ test_role }}'
+    description: Ansible Test Role {{ resource_prefix }}
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+    - iam_role.iam_role.description == 'Ansible Test Role {{ resource_prefix }}'
+
+- name: Add Description (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    description: Ansible Test Role {{ resource_prefix }}
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Add Description (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    description: Ansible Test Role {{ resource_prefix }}
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+    - iam_role.iam_role.role_name == test_role
+    - iam_role.iam_role.description == 'Ansible Test Role {{ resource_prefix }}'
+
+- name: iam_role_info after adding Description
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].description == "Ansible Test Role {{ resource_prefix
+      }}"
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 1
+    - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role
+    - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/"
+      + test_role)
+    - role_info.iam_roles[0].managed_policies | length == 0
+    - role_info.iam_roles[0].max_session_duration == 43200
+    - role_info.iam_roles[0].path == '/'
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 0
+
+# ------------------------------------------------------------------------------------------
+
+- name: Update Description (CHECK MODE)
+  iam_role:
+    name: '{{ test_role }}'
+    description: Ansible Test Role (updated) {{ resource_prefix }}
+  check_mode: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: Update Description
+  iam_role:
+    name: '{{ test_role }}'
+    description: Ansible Test Role (updated) {{ resource_prefix }}
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+    - iam_role.iam_role.description == 'Ansible Test Role (updated) {{ resource_prefix
+      }}'
+
+- name: Update Description (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    description: Ansible Test Role (updated) {{ resource_prefix }}
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Update Description (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    description: Ansible Test Role (updated) {{ resource_prefix }}
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+    - iam_role.iam_role.role_name == test_role
+    - iam_role.iam_role.description == 'Ansible Test Role (updated) {{ resource_prefix
+      }}'
+
+- name: iam_role_info after updating Description
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix
+      }}"
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 1
+    - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role
+    - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/"
+      + test_role)
+    - role_info.iam_roles[0].managed_policies | length == 0
+    - role_info.iam_roles[0].max_session_duration == 43200
+    - role_info.iam_roles[0].path == '/'
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 0
diff --git a/tests/integration/targets/iam_role/tasks/inline_policy_update.yml b/tests/integration/targets/iam_role/tasks/inline_policy_update.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/tasks/inline_policy_update.yml
@@ -0,0 +1,49 @@
+- name: Attach inline policy a
+  iam_policy:
+    state: present
+    iam_type: role
+    iam_name: '{{ test_role }}'
+    policy_name: inline-policy-a
+    policy_json: '{{ lookup("file", "deny-all-a.json") }}'
+- name: Attach inline policy b
+  iam_policy:
+    state: present
+    iam_type: role
+    iam_name: '{{ test_role }}'
+    policy_name: inline-policy-b
+    policy_json: '{{ lookup("file", "deny-all-b.json") }}'
+- name: iam_role_info after attaching inline policies (using iam_policy)
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix
+      }}"
+    - role_info.iam_roles[0].inline_policies | length == 2
+    - '"inline-policy-a" in role_info.iam_roles[0].inline_policies'
+    - '"inline-policy-b" in role_info.iam_roles[0].inline_policies'
+    - role_info.iam_roles[0].instance_profiles | length == 1
+    - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role
+    - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/"
+      + test_role)
+    - role_info.iam_roles[0].managed_policies | length == 1
+    - safe_managed_policy not in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name")
+      | list | flatten )
+    - custom_policy_name in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name")
+      | list | flatten )
+    - role_info.iam_roles[0].max_session_duration == 43200
+    - role_info.iam_roles[0].path == '/'
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 1
+    - '"TagB" in role_info.iam_roles[0].tags'
+    - role_info.iam_roles[0].tags.TagB == "ValueB"
diff --git a/tests/integration/targets/iam_role/tasks/main.yml b/tests/integration/targets/iam_role/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/tasks/main.yml
@@ -0,0 +1,83 @@
+# Tests for iam_role and iam_role_info
+#
+# Tests:
+# - Minimal Role creation
+# - Role deletion
+# - Fetching a specific role
+# - Creating roles w/ and w/o instance profiles
+# - Creating roles w/ a path
+# - Updating Max Session Duration
+# - Updating Description
+# - Managing list of managed policies
+# - Managing list of inline policies (for testing _info)
+# - Managing boundary policy
+#
+# Notes:
+# - Only tests *documented* return values ( RESULT.iam_role )
+# - There are some known timing issues with boto3 returning before actions
+#   complete in the case of problems with "changed" status it's worth enabling
+#   the standard_pauses and paranoid_pauses options as a first step in debugging
+
+
+- name: Setup AWS connection info
+  module_defaults:
+    group/aws:
+      access_key: '{{ aws_access_key }}'
+      secret_key: '{{ aws_secret_key }}'
+      session_token: '{{ security_token | default(omit) }}'
+      region: '{{ aws_region }}'
+    iam_role:
+      assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}'
+  collections:
+  - community.general
+  block:
+  - set_fact:
+      assume_deny_policy: '{{ lookup("file", "deny-assume.json") | from_json }}'
+  - include_tasks: parameter_checks.yml
+  - name: Create Safe IAM Managed Policy
+    iam_managed_policy:
+      state: present
+      policy_name: '{{ custom_policy_name }}'
+      policy_description: A safe (deny-all) managed policy
+      policy: "{{ lookup('file', 'deny-all.json') }}"
+    register: create_managed_policy
+  - assert:
+      that:
+      - create_managed_policy is succeeded
+
+    # ===================================================================
+    # Rapid Role Creation and deletion
+  - include_tasks: creation_deletion.yml
+  - include_tasks: max_session_update.yml
+  - include_tasks: description_update.yml
+  - include_tasks: tags_update.yml
+  - include_tasks: policy_update.yml
+  - include_tasks: inline_policy_update.yml
+  - include_tasks: role_removal.yml
+  - include_tasks: boundary_policy.yml
+  - include_tasks: complex_role_creation.yml
+  always:
+    # ===================================================================
+    # Cleanup
+
+  - name: Remove IAM Role
+    iam_role:
+      state: absent
+      name: '{{ test_role }}'
+      delete_instance_profile: yes
+    ignore_errors: true
+  - name: Remove IAM Role (with path)
+    iam_role:
+      state: absent
+      name: '{{ test_role }}'
+      path: '{{ test_path }}'
+      delete_instance_profile: yes
+    ignore_errors: true
+  - name: iam_role_info after Role deletion
+    iam_role_info:
+      name: '{{ test_role }}'
+    ignore_errors: true
+  - name: Remove test managed policy
+    iam_managed_policy:
+      state: absent
+      policy_name: '{{ custom_policy_name }}'
diff --git a/tests/integration/targets/iam_role/tasks/max_session_update.yml b/tests/integration/targets/iam_role/tasks/max_session_update.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/tasks/max_session_update.yml
@@ -0,0 +1,66 @@
+- name: Update Max Session Duration (CHECK MODE)
+  iam_role:
+    name: '{{ test_role }}'
+    max_session_duration: 43200
+  check_mode: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: Update Max Session Duration
+  iam_role:
+    name: '{{ test_role }}'
+    max_session_duration: 43200
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+    - iam_role.iam_role.max_session_duration == 43200
+
+- name: Update Max Session Duration (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    max_session_duration: 43200
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Update Max Session Duration (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    max_session_duration: 43200
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: iam_role_info after updating Max Session Duration
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - '"description" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 1
+    - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role
+    - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/"
+      + test_role)
+    - role_info.iam_roles[0].managed_policies | length == 0
+    - role_info.iam_roles[0].max_session_duration == 43200
+    - role_info.iam_roles[0].path == '/'
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 0
diff --git a/tests/integration/targets/iam_role/tasks/parameter_checks.yml b/tests/integration/targets/iam_role/tasks/parameter_checks.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/tasks/parameter_checks.yml
@@ -0,0 +1,82 @@
+# Parameter Checks
+- name: Friendly message when creating an instance profile and adding a boundary profile
+  iam_role:
+    name: '{{ test_role }}'
+    boundary: '{{ boundary_policy }}'
+  register: iam_role
+  ignore_errors: yes
+- assert:
+    that:
+    - iam_role is failed
+    - '"boundary policy" in iam_role.msg'
+    - '"create_instance_profile" in iam_role.msg'
+    - '"false" in iam_role.msg'
+
+- name: Friendly message when boundary profile is not an ARN
+  iam_role:
+    name: '{{ test_role }}'
+    boundary: AWSDenyAll
+    create_instance_profile: no
+  register: iam_role
+  ignore_errors: yes
+- assert:
+    that:
+    - iam_role is failed
+    - '"Boundary policy" in iam_role.msg'
+    - '"ARN" in iam_role.msg'
+
+- name: Friendly message when "present" without assume_role_policy_document
+  module_defaults: {iam_role: {}}
+  iam_role:
+    name: '{{ test_role }}'
+  register: iam_role
+  ignore_errors: yes
+- assert:
+    that:
+    - iam_role is failed
+    - iam_role.msg.startswith("state is present but all of the following are missing")
+    - '"assume_role_policy_document" in iam_role.msg'
+
+- name: Maximum Session Duration needs to be between 1 and 12 hours
+  iam_role:
+    name: '{{ test_role }}'
+    max_session_duration: 3599
+  register: iam_role
+  ignore_errors: yes
+- assert:
+    that:
+    - iam_role is failed
+    - '"max_session_duration must be between" in iam_role.msg'
+
+- name: Maximum Session Duration needs to be between 1 and 12 hours
+  iam_role:
+    name: '{{ test_role }}'
+    max_session_duration: 43201
+  register: iam_role
+  ignore_errors: yes
+- assert:
+    that:
+    - iam_role is failed
+    - '"max_session_duration must be between" in iam_role.msg'
+
+- name: Role Paths must start with /
+  iam_role:
+    name: '{{ test_role }}'
+    path: test/
+  register: iam_role
+  ignore_errors: yes
+- assert:
+    that:
+    - iam_role is failed
+    - '"path must begin and end with /" in iam_role.msg'
+
+- name: Role Paths must end with /
+  iam_role:
+    name: '{{ test_role }}'
+    path: /test
+  register: iam_role
+  ignore_errors: yes
+- assert:
+    that:
+    - iam_role is failed
+    - '"path must begin and end with /" in iam_role.msg'
diff --git a/tests/integration/targets/iam_role/tasks/policy_update.yml b/tests/integration/targets/iam_role/tasks/policy_update.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/tasks/policy_update.yml
@@ -0,0 +1,246 @@
+- name: Add Managed Policy (CHECK MODE)
+  iam_role:
+    name: '{{ test_role }}'
+    purge_policies: no
+    managed_policy:
+    - '{{ safe_managed_policy }}'
+  check_mode: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: Add Managed Policy
+  iam_role:
+    name: '{{ test_role }}'
+    purge_policies: no
+    managed_policy:
+    - '{{ safe_managed_policy }}'
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+
+- name: Add Managed Policy (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    purge_policies: no
+    managed_policy:
+    - '{{ safe_managed_policy }}'
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Add Managed Policy (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    purge_policies: no
+    managed_policy:
+    - '{{ safe_managed_policy }}'
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+    - iam_role.iam_role.role_name == test_role
+
+- name: iam_role_info after adding Managed Policy
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix
+      }}"
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 1
+    - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role
+    - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/"
+      + test_role)
+    - role_info.iam_roles[0].managed_policies | length == 1
+    - safe_managed_policy in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name")
+      | list | flatten )
+    - custom_policy_name not in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name")
+      | list | flatten )
+    - role_info.iam_roles[0].max_session_duration == 43200
+    - role_info.iam_roles[0].path == '/'
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 1
+    - '"TagB" in role_info.iam_roles[0].tags'
+    - role_info.iam_roles[0].tags.TagB == "ValueB"
+
+# ------------------------------------------------------------------------------------------
+
+- name: Update Managed Policy without purge (CHECK MODE)
+  iam_role:
+    name: '{{ test_role }}'
+    purge_policies: no
+    managed_policy:
+    - '{{ custom_policy_name }}'
+  check_mode: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: Update Managed Policy without purge
+  iam_role:
+    name: '{{ test_role }}'
+    purge_policies: no
+    managed_policy:
+    - '{{ custom_policy_name }}'
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+
+- name: Update Managed Policy without purge (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    purge_policies: no
+    managed_policy:
+    - '{{ custom_policy_name }}'
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Update Managed Policy without purge (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    purge_policies: no
+    managed_policy:
+    - '{{ custom_policy_name }}'
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+    - iam_role.iam_role.role_name == test_role
+
+- name: iam_role_info after updating Managed Policy without purge
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix
+      }}"
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 1
+    - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role
+    - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/"
+      + test_role)
+    - role_info.iam_roles[0].managed_policies | length == 2
+    - safe_managed_policy in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name")
+      | list | flatten )
+    - custom_policy_name in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name")
+      | list | flatten )
+    - role_info.iam_roles[0].max_session_duration == 43200
+    - role_info.iam_roles[0].path == '/'
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 1
+    - '"TagB" in role_info.iam_roles[0].tags'
+    - role_info.iam_roles[0].tags.TagB == "ValueB"
+
+# ------------------------------------------------------------------------------------------
+
+# Managed Policies are purged by default
+- name: Update Managed Policy with purge (CHECK MODE)
+  iam_role:
+    name: '{{ test_role }}'
+    managed_policy:
+    - '{{ custom_policy_name }}'
+  check_mode: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: Update Managed Policy with purge
+  iam_role:
+    name: '{{ test_role }}'
+    managed_policy:
+    - '{{ custom_policy_name }}'
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+
+- name: Update Managed Policy with purge (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    managed_policy:
+    - '{{ custom_policy_name }}'
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Update Managed Policy with purge (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    managed_policy:
+    - '{{ custom_policy_name }}'
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+    - iam_role.iam_role.role_name == test_role
+
+- name: iam_role_info after updating Managed Policy with purge
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix
+      }}"
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 1
+    - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role
+    - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/"
+      + test_role)
+    - role_info.iam_roles[0].managed_policies | length == 1
+    - safe_managed_policy not in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name")
+      | list | flatten )
+    - custom_policy_name in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name")
+      | list | flatten )
+    - role_info.iam_roles[0].max_session_duration == 43200
+    - role_info.iam_roles[0].path == '/'
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 1
+    - '"TagB" in role_info.iam_roles[0].tags'
+    - role_info.iam_roles[0].tags.TagB == "ValueB"
diff --git a/tests/integration/targets/iam_role/tasks/role_removal.yml b/tests/integration/targets/iam_role/tasks/role_removal.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/tasks/role_removal.yml
@@ -0,0 +1,59 @@
+- name: Remove IAM Role (CHECK MODE)
+  iam_role:
+    state: absent
+    name: '{{ test_role }}'
+    delete_instance_profile: yes
+  check_mode: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: iam_role_info after deleting role in check mode
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+
+- name: Remove IAM Role
+  iam_role:
+    state: absent
+    name: '{{ test_role }}'
+    delete_instance_profile: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: iam_role_info after deleting role
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 0
+
+- name: Remove IAM Role (should be gone already) - check mode
+  iam_role:
+    state: absent
+    name: '{{ test_role }}'
+    delete_instance_profile: yes
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Remove IAM Role (should be gone already)
+  iam_role:
+    state: absent
+    name: '{{ test_role }}'
+    delete_instance_profile: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
diff --git a/tests/integration/targets/iam_role/tasks/tags_update.yml b/tests/integration/targets/iam_role/tasks/tags_update.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/iam_role/tasks/tags_update.yml
@@ -0,0 +1,328 @@
+- name: Add Tag (CHECK MODE)
+  iam_role:
+    name: '{{ test_role }}'
+    tags:
+      TagA: ValueA
+  check_mode: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: Add Tag
+  iam_role:
+    name: '{{ test_role }}'
+    tags:
+      TagA: ValueA
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+    - iam_role.iam_role.tags | length == 1
+    - '"TagA" in iam_role.iam_role.tags'
+    - iam_role.iam_role.tags.TagA == "ValueA"
+
+- name: Add Tag (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    tags:
+      TagA: ValueA
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Add Tag (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    tags:
+      TagA: ValueA
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+    - iam_role.iam_role.role_name == test_role
+    - '"TagA" in iam_role.iam_role.tags'
+    - iam_role.iam_role.tags.TagA == "ValueA"
+
+- name: iam_role_info after adding Tags
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix
+      }}"
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 1
+    - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role
+    - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/"
+      + test_role)
+    - role_info.iam_roles[0].managed_policies | length == 0
+    - role_info.iam_roles[0].max_session_duration == 43200
+    - role_info.iam_roles[0].path == '/'
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 1
+    - '"TagA" in role_info.iam_roles[0].tags'
+    - role_info.iam_roles[0].tags.TagA == "ValueA"
+
+# ------------------------------------------------------------------------------------------
+
+- name: Update Tag (CHECK MODE)
+  iam_role:
+    name: '{{ test_role }}'
+    tags:
+      TagA: AValue
+  check_mode: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: Update Tag
+  iam_role:
+    name: '{{ test_role }}'
+    tags:
+      TagA: AValue
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+    - '"TagA" in iam_role.iam_role.tags'
+    - iam_role.iam_role.tags.TagA == "AValue"
+
+- name: Update Tag (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    tags:
+      TagA: AValue
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Update Tag (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    tags:
+      TagA: AValue
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+    - iam_role.iam_role.role_name == test_role
+    - '"TagA" in iam_role.iam_role.tags'
+    - iam_role.iam_role.tags.TagA == "AValue"
+
+- name: iam_role_info after updating Tag
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix
+      }}"
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 1
+    - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role
+    - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/"
+      + test_role)
+    - role_info.iam_roles[0].managed_policies | length == 0
+    - role_info.iam_roles[0].max_session_duration == 43200
+    - role_info.iam_roles[0].path == '/'
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 1
+    - '"TagA" in role_info.iam_roles[0].tags'
+    - role_info.iam_roles[0].tags.TagA == "AValue"
+
+# ------------------------------------------------------------------------------------------
+
+- name: Add second Tag without purge (CHECK MODE)
+  iam_role:
+    name: '{{ test_role }}'
+    purge_tags: no
+    tags:
+      TagB: ValueB
+  check_mode: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: Add second Tag without purge
+  iam_role:
+    name: '{{ test_role }}'
+    purge_tags: no
+    tags:
+      TagB: ValueB
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+    - '"TagB" in iam_role.iam_role.tags'
+    - iam_role.iam_role.tags.TagB == "ValueB"
+
+- name: Add second Tag without purge (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    purge_tags: no
+    tags:
+      TagB: ValueB
+  register: iam_role
+  check_mode: yes
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Add second Tag without purge (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    purge_tags: no
+    tags:
+      TagB: ValueB
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+    - iam_role.iam_role.role_name == test_role
+    - '"TagB" in iam_role.iam_role.tags'
+    - iam_role.iam_role.tags.TagB == "ValueB"
+
+- name: iam_role_info after adding second Tag without purge
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix
+      }}"
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 1
+    - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role
+    - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/"
+      + test_role)
+    - role_info.iam_roles[0].managed_policies | length == 0
+    - role_info.iam_roles[0].max_session_duration == 43200
+    - role_info.iam_roles[0].path == '/'
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 2
+    - '"TagA" in role_info.iam_roles[0].tags'
+    - role_info.iam_roles[0].tags.TagA == "AValue"
+    - '"TagB" in role_info.iam_roles[0].tags'
+    - role_info.iam_roles[0].tags.TagB == "ValueB"
+
+# ------------------------------------------------------------------------------------------
+
+- name: Purge first tag (CHECK MODE)
+  iam_role:
+    name: '{{ test_role }}'
+    purge_tags: yes
+    tags:
+      TagB: ValueB
+  check_mode: yes
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+
+- name: Purge first tag
+  iam_role:
+    name: '{{ test_role }}'
+    purge_tags: yes
+    tags:
+      TagB: ValueB
+  register: iam_role
+- assert:
+    that:
+    - iam_role is changed
+    - iam_role.iam_role.role_name == test_role
+    - '"TagB" in iam_role.iam_role.tags'
+    - iam_role.iam_role.tags.TagB == "ValueB"
+
+- name: Purge first tag (no change) - check mode
+  iam_role:
+    name: '{{ test_role }}'
+    purge_tags: yes
+    tags:
+      TagB: ValueB
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+
+- name: Purge first tag (no change)
+  iam_role:
+    name: '{{ test_role }}'
+    purge_tags: yes
+    tags:
+      TagB: ValueB
+  register: iam_role
+- assert:
+    that:
+    - iam_role is not changed
+    - iam_role.iam_role.role_name == test_role
+    - '"TagB" in iam_role.iam_role.tags'
+    - iam_role.iam_role.tags.TagB == "ValueB"
+
+- name: iam_role_info after purging first Tag
+  iam_role_info:
+    name: '{{ test_role }}'
+  register: role_info
+- assert:
+    that:
+    - role_info is succeeded
+    - role_info.iam_roles | length == 1
+    - role_info.iam_roles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].arn.endswith("role/" + test_role )
+    - '"assume_role_policy_document" in role_info.iam_roles[0]'
+    - '"create_date" in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix
+      }}"
+    - role_info.iam_roles[0].inline_policies | length == 0
+    - role_info.iam_roles[0].instance_profiles | length == 1
+    - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role
+    - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn")
+    - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/"
+      + test_role)
+    - role_info.iam_roles[0].managed_policies | length == 0
+    - role_info.iam_roles[0].max_session_duration == 43200
+    - role_info.iam_roles[0].path == '/'
+    - '"permissions_boundary" not in role_info.iam_roles[0]'
+    - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id
+    - role_info.iam_roles[0].role_name == test_role
+    - role_info.iam_roles[0].tags | length == 1
+    - '"TagA" not in role_info.iam_roles[0].tags'
+    - '"TagB" in role_info.iam_roles[0].tags'
+    - role_info.iam_roles[0].tags.TagB == "ValueB"