Repository: ansible-collections/amazon.aws

Issue:
ec2_group: error returned does not return AWS insufficient permissions error when adding tags 
##### SUMMARY
When using AWS IAM credentials that are granted insufficient AWS IAM permissions to add tags to an EC2 Security Group, ie, the `CreateTags` permission, you should received an error that the credentials were not authorised to perform the `CreateTags` operation.

Instead, the error returned is:
TypeError: fail_json() takes exactly 1 argument (3 given)

I searched for open issues for "ec2_group" and "fail_json", found nothing open.

Looked at devel branch of "amazon.aws/plugins/modules/ec2_group.py" and the 'update_tags' function still has the same line as tested version..

I am still using the latest version of ansible 2.9

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
"amazon.aws/plugins/modules/ec2_group.py"

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes -->
```paste below
ansible 2.9.15
  config file = /home/z3266423/.ansible.cfg
  configured module search path = ['/home/z3266423/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/z3266423/.virtualenvs/ansible_playbooks_survey_editor/lib64/python3.7/site-packages/ansible
  executable location = /home/z3266423/.virtualenvs/ansible_playbooks_survey_editor/bin/ansible
  python version = 3.7.7 (default, Mar 13 2020, 21:39:43) [GCC 9.2.1 20190827 (Red Hat 9.2.1-1)]
```

##### CONFIGURATION
<!--- Paste verbatim output from "ansible-config dump --only-changed" between quotes -->
```paste below
ANSIBLE_PIPELINING(/home/z3266423/.ansible.cfg) = True
DEFAULT_ROLES_PATH(env: ANSIBLE_ROLES_PATH) = ['/home/z3266423/Documents/ansible/roles/ces_survey_editor']
HOST_KEY_CHECKING(/home/z3266423/.ansible.cfg) = False
```

##### OS / ENVIRONMENT
<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. -->
```
]$ lsb_release -a
LSB Version:    :core-4.1-amd64:core-4.1-noarch
Distributor ID: Fedora
Description:    Fedora release 30 (Thirty)
Release:        30
Codename:       Thirty
```
##### STEPS TO REPRODUCE
<!--- Describe exactly how to reproduce the problem, using a minimal test-case -->
Deploy an EC2 Security Group using the 'ec2_group' module using AWS credentials that do not have the "CreateTags" permission

<!--- Paste example playbooks or commands between quotes below -->
```yaml
- ec2_group:
    state: 'present'
    aws_access_key: "{{ lookup('ENV', 'AWS_ACCESS_KEY_ID') }}"
    aws_secret_key: "{{ lookup('ENV', 'AWS_SECRET_ACCESS_KEY_ID') }}"
    security_token: "{{ lookup('ENV', 'AWS_SESSION_TOKEN') }}"
    region: 'ap-southeast-2'
    name: 'test-group'
    description: 'this is a test group'
    tags:
      name: test-group
```
AWS_ACCESS_KEY_ID used is missing the AWS IAM EC2 permission `CreateTags`

<!--- HINT: You can paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- Describe what you expected to happen when running the steps above -->
The return error should contain a message similar to:
```
ClientError: An error occurred (UnauthorizedOperation) when calling the CreateTags operation: You are not authorized to perform this operation. Encoded authorization failure message:
```
Followed by the encoded error message.

This correct error message was generated by manually editing:
```
ansible/modules/cloud/amazon/ec2_group.py
```
and changing the code for the `update_tags` function where the call to `fail_json()` is changed to `fail_json_aws()`.
Original line:
```
module.fail_json(e, msg="Unable to add tags {0}".format(tags_need_modify))
```
Changed line:
```
module.fail_json_aws(e, msg="Unable to add tags {0}".format(tags_need_modify))
```
Full function of `update_tags` before edit:
```
def update_tags(client, module, group_id, current_tags, tags, purge_tags):
    tags_need_modify, tags_to_delete = compare_aws_tags(current_tags, tags, purge_tags)

    if not module.check_mode:
        if tags_to_delete:
            try:
                client.delete_tags(Resources=[group_id], Tags=[{'Key': tag} for tag in tags_to_delete])
            except (BotoCoreError, ClientError) as e:
                module.fail_json_aws(e, msg="Unable to delete tags {0}".format(tags_to_delete))

        # Add/update tags
        if tags_need_modify:
            try:
                client.create_tags(Resources=[group_id], Tags=ansible_dict_to_boto3_tag_list(tags_need_modify))
            except (BotoCoreError, ClientError) as e:
                module.fail_json(e, msg="Unable to add tags {0}".format(tags_need_modify))

    return bool(tags_need_modify or tags_to_delete)
```
Full function of `update_tags` after edit:
```
def update_tags(client, module, group_id, current_tags, tags, purge_tags):
    tags_need_modify, tags_to_delete = compare_aws_tags(current_tags, tags, purge_tags)

    if not module.check_mode:
        if tags_to_delete:
            try:
                client.delete_tags(Resources=[group_id], Tags=[{'Key': tag} for tag in tags_to_delete])
            except (BotoCoreError, ClientError) as e:
                module.fail_json_aws(e, msg="Unable to delete tags {0}".format(tags_to_delete))

        # Add/update tags
        if tags_need_modify:
            try:
                client.create_tags(Resources=[group_id], Tags=ansible_dict_to_boto3_tag_list(tags_need_modify))
            except (BotoCoreError, ClientError) as e:
                module.fail_json_aws(e, msg="Unable to add tags {0}".format(tags_need_modify))

    return bool(tags_need_modify or tags_to_delete)
```
I humbly suggest that this change be added to this file.

##### ACTUAL RESULTS
<!--- Describe what actually happened. If possible run with extra verbosity (-vvvv) -->
An error indicating that the `fail_json()` function was given too many arguments is returned, similar to:
```
: FAILED! => {                                                                                                                                                        "changed": false,
    "module_stderr": "Traceback (most recent call last):\n  File \"<stdin>\", line 102, in <module>\n  File \"<stdin>\", line 94, in _ansiballz_main\n  File \"<stdin>\", line 40, in invoke_
module\n  File \"/usr/lib64/python2.7/runpy.py\", line 188, in run_module\n    fname, loader, pkg_name)\n  File \"/usr/lib64/python2.7/runpy.py\", line 82, in _run_module_code\n    mod_name
, mod_fname, mod_loader, pkg_name)\n  File \"/usr/lib64/python2.7/runpy.py\", line 72, in _run_code\n    exec code in run_globals\n  File \"/tmp/ansible_ec2_group_payload_5r2MgW/ansible_ec2
_group_payload.zip/ansible/modules/cloud/amazon/ec2_group.py\", line 1265, in <module>\n  File \"/tmp/ansible_ec2_group_payload_5r2MgW/ansible_ec2_group_payload.zip/ansible/modules/cloud/amazon/ec2_group.py\", line 1142, in main\n  File \"/tmp/ansible_ec2_group_payload_5r2MgW/ansible_ec2_group_payload.zip/ansible/modules/cloud/amazon/ec2_group.py\", line 796, in update_tags\n
  File \"/tmp/ansible_ec2_group_payload_5r2MgW/ansible_ec2_group_payload.zip/ansible/module_utils/aws/core.py\", line 163, in fail_json\nTypeError: fail_json() takes exactly 1 argument (3 g
iven)\n",
    "module_stdout": "",
    "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error",
```
is returned




Discussion:

Resolving patch:
diff --git a/plugins/modules/rds_instance.py b/plugins/modules/rds_instance.py
new file mode 100644
--- /dev/null
+++ b/plugins/modules/rds_instance.py
@@ -0,0 +1,1426 @@
+#!/usr/bin/python
+# Copyright (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: rds_instance
+version_added: 1.0.0
+short_description: Manage RDS instances
+description:
+    - Create, modify, and delete RDS instances.
+extends_documentation_fragment:
+    - amazon.aws.aws
+    - amazon.aws.ec2
+    - amazon.aws.tags
+author:
+    - Sloane Hertel (@s-hertel)
+
+options:
+  # General module options
+    state:
+        description:
+          - Whether the snapshot should exist or not. I(rebooted) is not idempotent and will leave the DB instance in a running state
+            and start it prior to rebooting if it was stopped. I(present) will leave the DB instance in the current running/stopped state,
+            (running if creating the DB instance).
+          - I(state=running) and I(state=started) are synonyms, as are I(state=rebooted) and I(state=restarted). Note - rebooting the instance
+            is not idempotent.
+        choices: ['present', 'absent', 'terminated', 'running', 'started', 'stopped', 'rebooted', 'restarted']
+        default: 'present'
+        type: str
+    creation_source:
+        description: Which source to use if restoring from a template (an existing instance, S3 bucket, or snapshot).
+        choices: ['snapshot', 's3', 'instance']
+        type: str
+    force_update_password:
+        description:
+          - Set to C(True) to update your instance password with I(master_user_password). Since comparing passwords to determine
+            if it needs to be updated is not possible this is set to False by default to allow idempotence.
+        type: bool
+        default: False
+    purge_cloudwatch_logs_exports:
+        description: Set to False to retain any enabled cloudwatch logs that aren't specified in the task and are associated with the instance.
+        type: bool
+        default: True
+    read_replica:
+        description:
+          - Set to C(False) to promote a read replica instance or true to create one. When creating a read replica C(creation_source) should
+            be set to 'instance' or not provided. C(source_db_instance_identifier) must be provided with this option.
+        type: bool
+    wait:
+        description:
+          - Whether to wait for the instance to be available, stopped, or deleted. At a later time a I(wait_timeout) option may be added.
+            Following each API call to create/modify/delete the instance a waiter is used with a 60 second delay 30 times until the instance reaches
+            the expected state (available/stopped/deleted). The total task time may also be influenced by AWSRetry which helps stabilize if the
+            instance is in an invalid state to operate on to begin with (such as if you try to stop it when it is in the process of rebooting).
+            If setting this to False task retries and delays may make your playbook execution better handle timeouts for major modifications.
+        type: bool
+        default: True
+
+    # Options that have a corresponding boto3 parameter
+    allocated_storage:
+        description:
+          - The amount of storage (in gibibytes) to allocate for the DB instance.
+        type: int
+    allow_major_version_upgrade:
+        description:
+          - Whether to allow major version upgrades.
+        type: bool
+    apply_immediately:
+        description:
+          - A value that specifies whether modifying an instance with I(new_db_instance_identifier) and I(master_user_password)
+            should be applied as soon as possible, regardless of the I(preferred_maintenance_window) setting. If false, changes
+            are applied during the next maintenance window.
+        type: bool
+        default: False
+    auto_minor_version_upgrade:
+        description:
+          - Whether minor version upgrades are applied automatically to the DB instance during the maintenance window.
+        type: bool
+    availability_zone:
+        description:
+          - A list of EC2 Availability Zones that the DB instance can be created in.
+            May be used when creating an instance or when restoring from S3 or a snapshot. Mutually exclusive with I(multi_az).
+        aliases:
+          - az
+          - zone
+        type: str
+    backup_retention_period:
+        description:
+          - The number of days for which automated backups are retained.
+          - When set to C(0), automated backups will be disabled. (Not applicable if the DB instance is a source to read replicas)
+          - May be used when creating a new instance, when restoring from S3, or when modifying an instance.
+        type: int
+    ca_certificate_identifier:
+        description:
+          - The identifier of the CA certificate for the DB instance.
+        type: str
+    character_set_name:
+        description:
+          - The character set to associate with the DB instance.
+        type: str
+    copy_tags_to_snapshot:
+        description:
+          - Whether or not to copy all tags from the DB instance to snapshots of the instance. When initially creating
+            a DB instance the RDS API defaults this to false if unspecified.
+        type: bool
+    db_cluster_identifier:
+        description:
+          - The DB cluster (lowercase) identifier to add the aurora DB instance to. The identifier must contain from 1 to
+            63 letters, numbers, or hyphens and the first character must be a letter and may not end in a hyphen or
+            contain consecutive hyphens.
+        aliases:
+          - cluster_id
+        type: str
+    db_instance_class:
+        description:
+          - The compute and memory capacity of the DB instance, for example db.t2.micro.
+        aliases:
+          - class
+          - instance_type
+        type: str
+    db_instance_identifier:
+        description:
+          - The DB instance (lowercase) identifier. The identifier must contain from 1 to 63 letters, numbers, or
+            hyphens and the first character must be a letter and may not end in a hyphen or contain consecutive hyphens.
+        aliases:
+          - instance_id
+          - id
+        required: True
+        type: str
+    db_name:
+        description:
+          - The name for your database. If a name is not provided Amazon RDS will not create a database.
+        type: str
+    db_parameter_group_name:
+        description:
+          - The name of the DB parameter group to associate with this DB instance. When creating the DB instance if this
+            argument is omitted the default DBParameterGroup for the specified engine is used.
+        type: str
+    db_security_groups:
+        description:
+          - (EC2-Classic platform) A list of DB security groups to associate with this DB instance.
+        type: list
+        elements: str
+    db_snapshot_identifier:
+        description:
+          - The identifier or ARN of the DB snapshot to restore from when using I(creation_source=snapshot).
+        type: str
+        aliases:
+          - snapshot_identifier
+          - snapshot_id
+    db_subnet_group_name:
+        description:
+          - The DB subnet group name to use for the DB instance.
+        aliases:
+          - subnet_group
+        type: str
+    deletion_protection:
+        description:
+          -  A value that indicates whether the DB instance has deletion protection enabled.
+             The database can't be deleted when deletion protection is enabled.
+             By default, deletion protection is disabled.
+        type: bool
+        version_added: 3.3.0
+    domain:
+        description:
+          - The Active Directory Domain to restore the instance in.
+        type: str
+    domain_iam_role_name:
+        description:
+          - The name of the IAM role to be used when making API calls to the Directory Service.
+        type: str
+    enable_cloudwatch_logs_exports:
+        description:
+          - A list of log types that need to be enabled for exporting to CloudWatch Logs.
+        aliases:
+          - cloudwatch_log_exports
+        type: list
+        elements: str
+    enable_iam_database_authentication:
+        description:
+          - Enable mapping of AWS Identity and Access Management (IAM) accounts to database accounts.
+            If this option is omitted when creating the instance, Amazon RDS sets this to False.
+        type: bool
+    enable_performance_insights:
+        description:
+          - Whether to enable Performance Insights for the DB instance.
+        type: bool
+    engine:
+        description:
+          - The name of the database engine to be used for this DB instance. This is required to create an instance.
+        choices: ['aurora', 'aurora-mysql', 'aurora-postgresql', 'mariadb', 'mysql', 'oracle-ee', 'oracle-ee-cdb',
+                  'oracle-se2', 'oracle-se2-cdb', 'postgres', 'sqlserver-ee', 'sqlserver-se', 'sqlserver-ex', 'sqlserver-web']
+        type: str
+    engine_version:
+        description:
+          - The version number of the database engine to use. For Aurora MySQL that could be 5.6.10a , 5.7.12.
+            Aurora PostgreSQL example, 9.6.3
+        type: str
+    final_db_snapshot_identifier:
+        description:
+          - The DB instance snapshot identifier of the new DB instance snapshot created when I(skip_final_snapshot) is false.
+        aliases:
+          - final_snapshot_identifier
+        type: str
+    force_failover:
+        description:
+          - Set to true to conduct the reboot through a MultiAZ failover.
+        type: bool
+    iam_roles:
+        description:
+          - List of Amazon Web Services Identity and Access Management (IAM) roles to associate with DB instance.
+        type: list
+        elements: dict
+        suboptions:
+          feature_name:
+            description:
+              - The name of the feature associated with the IAM role.
+            type: str
+            required: true
+          role_arn:
+            description:
+              - The ARN of the IAM role to associate with the DB instance.
+            type: str
+            required: true
+        version_added: 3.3.0
+    iops:
+        description:
+          - The Provisioned IOPS (I/O operations per second) value. Is only set when using I(storage_type) is set to io1.
+        type: int
+    kms_key_id:
+        description:
+          - The ARN of the AWS KMS key identifier for an encrypted DB instance. If you are creating a DB instance with the
+            same AWS account that owns the KMS encryption key used to encrypt the new DB instance, then you can use the KMS key
+            alias instead of the ARN for the KM encryption key.
+          - If I(storage_encrypted) is true and and this option is not provided, the default encryption key is used.
+        type: str
+    license_model:
+        description:
+          - The license model for the DB instance.
+          - Several options are license-included, bring-your-own-license, and general-public-license.
+          - This option can also be omitted to default to an accepted value.
+        type: str
+    master_user_password:
+        description:
+          - An 8-41 character password for the master database user. The password can contain any printable ASCII character
+            except "/", """, or "@". To modify the password use I(force_update_password). Use I(apply immediately) to change
+            the password immediately, otherwise it is updated during the next maintenance window.
+        aliases:
+          - password
+        type: str
+    master_username:
+        description:
+          - The name of the master user for the DB instance. Must be 1-16 letters or numbers and begin with a letter.
+        aliases:
+          - username
+        type: str
+    max_allocated_storage:
+        description:
+          - The upper limit to which Amazon RDS can automatically scale the storage of the DB instance.
+        type: int
+    monitoring_interval:
+        description:
+          - The interval, in seconds, when Enhanced Monitoring metrics are collected for the DB instance. To disable collecting
+            metrics, specify 0. Amazon RDS defaults this to 0 if omitted when initially creating a DB instance.
+        type: int
+    monitoring_role_arn:
+        description:
+          - The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to Amazon CloudWatch Logs.
+        type: str
+    multi_az:
+        description:
+          - Specifies if the DB instance is a Multi-AZ deployment. Mutually exclusive with I(availability_zone).
+        type: bool
+    new_db_instance_identifier:
+        description:
+          - The new DB instance (lowercase) identifier for the DB instance when renaming a DB instance. The identifier must contain
+            from 1 to 63 letters, numbers, or hyphens and the first character must be a letter and may not end in a hyphen or
+            contain consecutive hyphens. Use I(apply_immediately) to rename immediately, otherwise it is updated during the
+            next maintenance window.
+        aliases:
+          - new_instance_id
+          - new_id
+        type: str
+    option_group_name:
+        description:
+          - The option group to associate with the DB instance.
+        type: str
+    performance_insights_kms_key_id:
+        description:
+          - The AWS KMS key identifier (ARN, name, or alias) for encryption of Performance Insights data.
+        type: str
+    performance_insights_retention_period:
+        description:
+          - The amount of time, in days, to retain Performance Insights data. Valid values are 7 or 731.
+        type: int
+    port:
+        description:
+          - The port number on which the instances accept connections.
+        type: int
+    preferred_backup_window:
+        description:
+          - The daily time range (in UTC) of at least 30 minutes, during which automated backups are created if automated backups are
+            enabled using I(backup_retention_period). The option must be in the format of "hh24:mi-hh24:mi" and not conflict with
+            I(preferred_maintenance_window).
+        aliases:
+          - backup_window
+        type: str
+    preferred_maintenance_window:
+        description:
+          - The weekly time range (in UTC) of at least 30 minutes, during which system maintenance can occur. The option must
+            be in the format "ddd:hh24:mi-ddd:hh24:mi" where ddd is one of Mon, Tue, Wed, Thu, Fri, Sat, Sun.
+        aliases:
+          - maintenance_window
+        type: str
+    processor_features:
+        description:
+          - A dictionary of Name, Value pairs to indicate the number of CPU cores and the number of threads per core for the
+            DB instance class of the DB instance. Names are threadsPerCore and coreCount.
+            Set this option to an empty dictionary to use the default processor features.
+        suboptions:
+          threadsPerCore:
+            description: The number of threads per core
+          coreCount:
+            description: The number of CPU cores
+        type: dict
+    promotion_tier:
+        description:
+          - An integer that specifies the order in which an Aurora Replica is promoted to the primary instance after a failure of
+            the existing primary instance.
+        type: str
+    publicly_accessible:
+        description:
+          - Specifies the accessibility options for the DB instance. A value of true specifies an Internet-facing instance with
+            a publicly resolvable DNS name, which resolves to a public IP address. A value of false specifies an internal
+            instance with a DNS name that resolves to a private IP address.
+        type: bool
+    purge_iam_roles:
+        description:
+          - Set to C(True) to remove any IAM roles that aren't specified in the task and are associated with the instance.
+        type: bool
+        default: False
+        version_added: 3.3.0
+    restore_time:
+        description:
+          - If using I(creation_source=instance) this indicates the UTC date and time to restore from the source instance.
+            For example, "2009-09-07T23:45:00Z".
+          - May alternatively set I(use_latest_restore_time=True).
+          - Only one of I(use_latest_restorable_time) and I(restore_time) may be provided.
+        type: str
+    s3_bucket_name:
+        description:
+          - The name of the Amazon S3 bucket that contains the data used to create the Amazon DB instance.
+        type: str
+    s3_ingestion_role_arn:
+        description:
+          - The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that authorizes Amazon RDS to access
+            the Amazon S3 bucket on your behalf.
+        type: str
+    s3_prefix:
+        description:
+          - The prefix for all of the file names that contain the data used to create the Amazon DB instance. If you do not
+            specify a SourceS3Prefix value, then the Amazon DB instance is created by using all of the files in the Amazon S3 bucket.
+        type: str
+    skip_final_snapshot:
+        description:
+          - Whether a final DB instance snapshot is created before the DB instance is deleted. If this is false I(final_db_snapshot_identifier)
+            must be provided.
+        type: bool
+        default: false
+    source_db_instance_identifier:
+        description:
+          - The identifier or ARN of the source DB instance from which to restore when creating a read replica or spinning up a point-in-time
+            DB instance using I(creation_source=instance). If the source DB is not in the same region this should be an ARN.
+        type: str
+    source_engine:
+        description:
+          - The identifier for the database engine that was backed up to create the files stored in the Amazon S3 bucket.
+        choices:
+          - mysql
+        type: str
+    source_engine_version:
+        description:
+          - The version of the database that the backup files were created from.
+        type: str
+    source_region:
+        description:
+          - The region of the DB instance from which the replica is created.
+        type: str
+    storage_encrypted:
+        description:
+          - Whether the DB instance is encrypted.
+        type: bool
+    storage_type:
+        description:
+          - The storage type to be associated with the DB instance. I(storage_type) does not apply to Aurora DB instances.
+        choices:
+          - standard
+          - gp2
+          - io1
+        type: str
+    tde_credential_arn:
+        description:
+          - The ARN from the key store with which to associate the instance for Transparent Data Encryption. This is
+            supported by Oracle or SQL Server DB instances and may be used in conjunction with C(storage_encrypted)
+            though it might slightly affect the performance of your database.
+        aliases:
+          - transparent_data_encryption_arn
+        type: str
+    tde_credential_password:
+        description:
+          - The password for the given ARN from the key store in order to access the device.
+        aliases:
+          - transparent_data_encryption_password
+        type: str
+    timezone:
+        description:
+          - The time zone of the DB instance.
+        type: str
+    use_latest_restorable_time:
+        description:
+          - Whether to restore the DB instance to the latest restorable backup time.
+          - Only one of I(use_latest_restorable_time) and I(restore_time) may be provided.
+        type: bool
+        aliases:
+          - restore_from_latest
+    vpc_security_group_ids:
+        description:
+          - A list of EC2 VPC security groups to associate with the DB instance.
+        type: list
+        elements: str
+    purge_security_groups:
+        description:
+          - Set to False to retain any enabled security groups that aren't specified in the task and are associated with the instance.
+          - Can be applied to I(vpc_security_group_ids) and I(db_security_groups)
+        type: bool
+        default: True
+        version_added: 1.5.0
+'''
+
+EXAMPLES = r'''
+# Note: These examples do not set authentication details, see the AWS Guide for details.
+- name: create minimal aurora instance in default VPC and default subnet group
+  amazon.aws.rds_instance:
+    engine: aurora
+    db_instance_identifier: ansible-test-aurora-db-instance
+    instance_type: db.t2.small
+    password: "{{ password }}"
+    username: "{{ username }}"
+    cluster_id: ansible-test-cluster  # This cluster must exist - see rds_cluster to manage it
+
+- name: Create a DB instance using the default AWS KMS encryption key
+  amazon.aws.rds_instance:
+    id: test-encrypted-db
+    state: present
+    engine: mariadb
+    storage_encrypted: True
+    db_instance_class: db.t2.medium
+    username: "{{ username }}"
+    password: "{{ password }}"
+    allocated_storage: "{{ allocated_storage }}"
+
+- name: remove the DB instance without a final snapshot
+  amazon.aws.rds_instance:
+    id: "{{ instance_id }}"
+    state: absent
+    skip_final_snapshot: True
+
+- name: remove the DB instance with a final snapshot
+  amazon.aws.rds_instance:
+    id: "{{ instance_id }}"
+    state: absent
+    final_snapshot_identifier: "{{ snapshot_id }}"
+
+- name: Add a new security group without purge
+  amazon.aws.rds_instance:
+    id: "{{ instance_id }}"
+    state: present
+    vpc_security_group_ids:
+      - sg-0be17ba10c9286b0b
+    purge_security_groups: false
+  register: result
+
+# Add IAM role to db instance
+- name: Create IAM policy
+  community.aws.iam_managed_policy:
+    policy_name: "my-policy"
+    policy: "{{ lookup('file','files/policy.json') }}"
+    state: present
+  register: iam_policy
+
+- name: Create IAM role
+  community.aws.iam_role:
+    assume_role_policy_document: "{{ lookup('file','files/assume_policy.json') }}"
+    name: "my-role"
+    state: present
+    managed_policy: "{{ iam_policy.policy.arn }}"
+  register: iam_role
+
+- name: Create DB instance with added IAM role
+  amazon.aws.rds_instance:
+    id: "my-instance-id"
+    state: present
+    engine: postgres
+    engine_version: 14.2
+    username: "{{ username }}"
+    password: "{{ password }}"
+    db_instance_class: db.m6g.large
+    allocated_storage: "{{ allocated_storage }}"
+    iam_roles:
+      - role_arn: "{{ iam_role.arn }}"
+        feature_name: 's3Export'
+
+- name: Remove IAM role from DB instance
+  amazon.aws.rds_instance:
+    id: "my-instance-id"
+    state: present
+    purge_iam_roles: true
+
+# Restore DB instance from snapshot
+- name: Create a snapshot and wait until completion
+  amazon.aws.rds_instance_snapshot:
+    instance_id: 'my-instance-id'
+    snapshot_id: 'my-new-snapshot'
+    state: present
+    wait: true
+  register: snapshot
+
+- name: Restore DB from snapshot
+  amazon.aws.rds_instance:
+    id: 'my-restored-db'
+    creation_source: snapshot
+    snapshot_identifier: 'my-new-snapshot'
+    engine: mariadb
+    state: present
+  register: restored_db
+'''
+
+RETURN = r'''
+allocated_storage:
+  description: The allocated storage size in gigabytes. This is always 1 for aurora database engines.
+  returned: always
+  type: int
+  sample: 20
+associated_roles:
+  description: The list of currently associated roles.
+  returned: always
+  type: list
+  sample: []
+auto_minor_version_upgrade:
+  description: Whether minor engine upgrades are applied automatically to the DB instance during the maintenance window.
+  returned: always
+  type: bool
+  sample: true
+availability_zone:
+  description: The availability zone for the DB instance.
+  returned: always
+  type: str
+  sample: us-east-1f
+backup_retention_period:
+  description: The number of days for which automated backups are retained.
+  returned: always
+  type: int
+  sample: 1
+ca_certificate_identifier:
+  description: The identifier of the CA certificate for the DB instance.
+  returned: always
+  type: str
+  sample: rds-ca-2015
+copy_tags_to_snapshot:
+  description: Whether tags are copied from the DB instance to snapshots of the DB instance.
+  returned: always
+  type: bool
+  sample: false
+db_instance_arn:
+  description: The Amazon Resource Name (ARN) for the DB instance.
+  returned: always
+  type: str
+  sample: arn:aws:rds:us-east-1:123456789012:db:ansible-test
+db_instance_class:
+  description: The name of the compute and memory capacity class of the DB instance.
+  returned: always
+  type: str
+  sample: db.m4.large
+db_instance_identifier:
+  description: The identifier of the DB instance
+  returned: always
+  type: str
+  sample: ansible-test
+db_instance_port:
+  description: The port that the DB instance listens on.
+  returned: always
+  type: int
+  sample: 0
+db_instance_status:
+  description: The current state of this database.
+  returned: always
+  type: str
+  sample: stopped
+db_parameter_groups:
+  description: The list of DB parameter groups applied to this DB instance.
+  returned: always
+  type: complex
+  contains:
+    db_parameter_group_name:
+      description: The name of the DP parameter group.
+      returned: always
+      type: str
+      sample: default.mariadb10.0
+    parameter_apply_status:
+      description: The status of parameter updates.
+      returned: always
+      type: str
+      sample: in-sync
+db_security_groups:
+  description: A list of DB security groups associated with this DB instance.
+  returned: always
+  type: list
+  sample: []
+db_subnet_group:
+  description: The subnet group associated with the DB instance.
+  returned: always
+  type: complex
+  contains:
+    db_subnet_group_description:
+      description: The description of the DB subnet group.
+      returned: always
+      type: str
+      sample: default
+    db_subnet_group_name:
+      description: The name of the DB subnet group.
+      returned: always
+      type: str
+      sample: default
+    subnet_group_status:
+      description: The status of the DB subnet group.
+      returned: always
+      type: str
+      sample: Complete
+    subnets:
+      description: A list of Subnet elements.
+      returned: always
+      type: complex
+      contains:
+        subnet_availability_zone:
+          description: The availability zone of the subnet.
+          returned: always
+          type: complex
+          contains:
+            name:
+              description: The name of the Availability Zone.
+              returned: always
+              type: str
+              sample: us-east-1c
+        subnet_identifier:
+          description: The ID of the subnet.
+          returned: always
+          type: str
+          sample: subnet-12345678
+        subnet_status:
+          description: The status of the subnet.
+          returned: always
+          type: str
+          sample: Active
+    vpc_id:
+      description: The VpcId of the DB subnet group.
+      returned: always
+      type: str
+      sample: vpc-12345678
+dbi_resource_id:
+  description: The AWS Region-unique, immutable identifier for the DB instance.
+  returned: always
+  type: str
+  sample: db-UHV3QRNWX4KB6GALCIGRML6QFA
+deletion_protection:
+  description: C(True) if the DB instance has deletion protection enabled, C(False) if not.
+  returned: always
+  type: bool
+  sample: False
+  version_added: 3.3.0
+domain_memberships:
+  description: The Active Directory Domain membership records associated with the DB instance.
+  returned: always
+  type: list
+  sample: []
+endpoint:
+  description: The connection endpoint.
+  returned: always
+  type: complex
+  contains:
+    address:
+      description: The DNS address of the DB instance.
+      returned: always
+      type: str
+      sample: ansible-test.cvlrtwiennww.us-east-1.rds.amazonaws.com
+    hosted_zone_id:
+      description: The ID that Amazon Route 53 assigns when you create a hosted zone.
+      returned: always
+      type: str
+      sample: ZTR2ITUGPA61AM
+    port:
+      description: The port that the database engine is listening on.
+      returned: always
+      type: int
+      sample: 3306
+engine:
+  description: The database engine version.
+  returned: always
+  type: str
+  sample: mariadb
+engine_version:
+  description: The database engine version.
+  returned: always
+  type: str
+  sample: 10.0.35
+iam_database_authentication_enabled:
+  description: Whether mapping of AWS Identity and Access Management (IAM) accounts to database accounts is enabled.
+  returned: always
+  type: bool
+  sample: false
+instance_create_time:
+  description: The date and time the DB instance was created.
+  returned: always
+  type: str
+  sample: '2018-07-04T16:48:35.332000+00:00'
+kms_key_id:
+  description: The AWS KMS key identifier for the encrypted DB instance when storage_encrypted is true.
+  returned: When storage_encrypted is true
+  type: str
+  sample: arn:aws:kms:us-east-1:123456789012:key/70c45553-ad2e-4a85-9f14-cfeb47555c33
+latest_restorable_time:
+  description: The latest time to which a database can be restored with point-in-time restore.
+  returned: always
+  type: str
+  sample: '2018-07-04T16:50:50.642000+00:00'
+license_model:
+  description: The License model information for this DB instance.
+  returned: always
+  type: str
+  sample: general-public-license
+master_username:
+  description: The master username for the DB instance.
+  returned: always
+  type: str
+  sample: test
+max_allocated_storage:
+  description: The upper limit to which Amazon RDS can automatically scale the storage of the DB instance.
+  returned: When max allocated storage is present.
+  type: int
+  sample: 100
+monitoring_interval:
+  description:
+    - The interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB instance.
+      0 means collecting Enhanced Monitoring metrics is disabled.
+  returned: always
+  type: int
+  sample: 0
+multi_az:
+  description: Whether the DB instance is a Multi-AZ deployment.
+  returned: always
+  type: bool
+  sample: false
+option_group_memberships:
+  description: The list of option group memberships for this DB instance.
+  returned: always
+  type: complex
+  contains:
+    option_group_name:
+      description: The name of the option group that the instance belongs to.
+      returned: always
+      type: str
+      sample: default:mariadb-10-0
+    status:
+      description: The status of the DB instance's option group membership.
+      returned: always
+      type: str
+      sample: in-sync
+pending_modified_values:
+  description: The changes to the DB instance that are pending.
+  returned: always
+  type: complex
+  contains: {}
+performance_insights_enabled:
+  description: True if Performance Insights is enabled for the DB instance, and otherwise false.
+  returned: always
+  type: bool
+  sample: false
+preferred_backup_window:
+  description: The daily time range during which automated backups are created if automated backups are enabled.
+  returned: always
+  type: str
+  sample: 07:01-07:31
+preferred_maintenance_window:
+  description: The weekly time range (in UTC) during which system maintenance can occur.
+  returned: always
+  type: str
+  sample: sun:09:31-sun:10:01
+publicly_accessible:
+  description:
+    - True for an Internet-facing instance with a publicly resolvable DNS name, False to indicate an
+      internal instance with a DNS name that resolves to a private IP address.
+  returned: always
+  type: bool
+  sample: true
+read_replica_db_instance_identifiers:
+  description: Identifiers of the Read Replicas associated with this DB instance.
+  returned: always
+  type: list
+  sample: []
+storage_encrypted:
+  description: Whether the DB instance is encrypted.
+  returned: always
+  type: bool
+  sample: false
+storage_type:
+  description: The storage type to be associated with the DB instance.
+  returned: always
+  type: str
+  sample: standard
+tags:
+  description: A dictionary of tags associated with the DB instance.
+  returned: always
+  type: complex
+  contains: {}
+vpc_security_groups:
+  description: A list of VPC security group elements that the DB instance belongs to.
+  returned: always
+  type: complex
+  contains:
+    status:
+      description: The status of the VPC security group.
+      returned: always
+      type: str
+      sample: active
+    vpc_security_group_id:
+      description: The name of the VPC security group.
+      returned: always
+      type: str
+      sample: sg-12345678
+'''
+
+from time import sleep
+
+try:
+    import botocore
+except ImportError:
+    pass  # caught by AnsibleAWSModule
+
+
+from ansible.module_utils._text import to_text
+from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
+from ansible.module_utils.six import string_types
+
+from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
+from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code
+from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_message
+from ansible_collections.amazon.aws.plugins.module_utils.core import get_boto3_client_method_parameters
+from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_tag_list
+from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry
+from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict
+from ansible_collections.amazon.aws.plugins.module_utils.rds import arg_spec_to_rds_params
+from ansible_collections.amazon.aws.plugins.module_utils.rds import call_method
+from ansible_collections.amazon.aws.plugins.module_utils.rds import compare_iam_roles
+from ansible_collections.amazon.aws.plugins.module_utils.rds import ensure_tags
+from ansible_collections.amazon.aws.plugins.module_utils.rds import get_final_identifier
+from ansible_collections.amazon.aws.plugins.module_utils.rds import get_rds_method_attribute
+from ansible_collections.amazon.aws.plugins.module_utils.rds import get_tags
+from ansible_collections.amazon.aws.plugins.module_utils.rds import update_iam_roles
+
+
+valid_engines = ['aurora', 'aurora-mysql', 'aurora-postgresql', 'mariadb', 'mysql', 'oracle-ee', 'oracle-ee-cdb',
+                 'oracle-se2', 'oracle-se2-cdb', 'postgres', 'sqlserver-ee', 'sqlserver-se', 'sqlserver-ex', 'sqlserver-web']
+
+valid_engines_iam_roles = ['aurora-postgresql', 'oracle-ee', 'oracle-ee-cdb', 'oracle-se2', 'oracle-se2-cdb',
+                           'postgres', 'sqlserver-ee', 'sqlserver-se', 'sqlserver-ex', 'sqlserver-web']
+
+
+def get_rds_method_attribute_name(instance, state, creation_source, read_replica):
+    method_name = None
+    if state == 'absent' or state == 'terminated':
+        if instance and instance['DBInstanceStatus'] not in ['deleting', 'deleted']:
+            method_name = 'delete_db_instance'
+    else:
+        if instance:
+            method_name = 'modify_db_instance'
+        elif read_replica is True:
+            method_name = 'create_db_instance_read_replica'
+        elif creation_source == 'snapshot':
+            method_name = 'restore_db_instance_from_db_snapshot'
+        elif creation_source == 's3':
+            method_name = 'restore_db_instance_from_s3'
+        elif creation_source == 'instance':
+            method_name = 'restore_db_instance_to_point_in_time'
+        else:
+            method_name = 'create_db_instance'
+    return method_name
+
+
+def get_instance(client, module, db_instance_id):
+    try:
+        for i in range(3):
+            try:
+                instance = client.describe_db_instances(DBInstanceIdentifier=db_instance_id)['DBInstances'][0]
+                instance['Tags'] = get_tags(client, module, instance['DBInstanceArn'])
+                if instance.get('ProcessorFeatures'):
+                    instance['ProcessorFeatures'] = dict((feature['Name'], feature['Value']) for feature in instance['ProcessorFeatures'])
+                if instance.get('PendingModifiedValues', {}).get('ProcessorFeatures'):
+                    instance['PendingModifiedValues']['ProcessorFeatures'] = dict(
+                        (feature['Name'], feature['Value'])
+                        for feature in instance['PendingModifiedValues']['ProcessorFeatures']
+                    )
+                break
+            except is_boto3_error_code('DBInstanceNotFound'):
+                sleep(3)
+        else:
+            instance = {}
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:  # pylint: disable=duplicate-except
+        module.fail_json_aws(e, msg='Failed to describe DB instances')
+    return instance
+
+
+def get_final_snapshot(client, module, snapshot_identifier):
+    try:
+        snapshots = AWSRetry.jittered_backoff()(client.describe_db_snapshots)(DBSnapshotIdentifier=snapshot_identifier)
+        if len(snapshots.get('DBSnapshots', [])) == 1:
+            return snapshots['DBSnapshots'][0]
+        return {}
+    except is_boto3_error_code('DBSnapshotNotFound') as e:  # May not be using wait: True
+        return {}
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:  # pylint: disable=duplicate-except
+        module.fail_json_aws(e, msg='Failed to retrieve information about the final snapshot')
+
+
+def get_parameters(client, module, parameters, method_name):
+    if method_name == 'restore_db_instance_to_point_in_time':
+        parameters['TargetDBInstanceIdentifier'] = module.params['db_instance_identifier']
+
+    required_options = get_boto3_client_method_parameters(client, method_name, required=True)
+    if any(parameters.get(k) is None for k in required_options):
+        module.fail_json(msg='To {0} requires the parameters: {1}'.format(
+            get_rds_method_attribute(method_name, module).operation_description, required_options))
+    options = get_boto3_client_method_parameters(client, method_name)
+    parameters = dict((k, v) for k, v in parameters.items() if k in options and v is not None)
+
+    if parameters.get('ProcessorFeatures') is not None:
+        parameters['ProcessorFeatures'] = [{'Name': k, 'Value': to_text(v)} for k, v in parameters['ProcessorFeatures'].items()]
+
+    # If this parameter is an empty list it can only be used with modify_db_instance (as the parameter UseDefaultProcessorFeatures)
+    if parameters.get('ProcessorFeatures') == [] and not method_name == 'modify_db_instance':
+        parameters.pop('ProcessorFeatures')
+
+    if method_name in ['create_db_instance', 'create_db_instance_read_replica', 'restore_db_instance_from_db_snapshot']:
+        if parameters.get('Tags'):
+            parameters['Tags'] = ansible_dict_to_boto3_tag_list(parameters['Tags'])
+
+    if method_name == 'modify_db_instance':
+        parameters = get_options_with_changing_values(client, module, parameters)
+
+    return parameters
+
+
+def get_options_with_changing_values(client, module, parameters):
+    instance_id = module.params['db_instance_identifier']
+    purge_cloudwatch_logs = module.params['purge_cloudwatch_logs_exports']
+    force_update_password = module.params['force_update_password']
+    port = module.params['port']
+    apply_immediately = parameters.pop('ApplyImmediately', None)
+    cloudwatch_logs_enabled = module.params['enable_cloudwatch_logs_exports']
+    purge_security_groups = module.params['purge_security_groups']
+
+    if port:
+        parameters['DBPortNumber'] = port
+    if not force_update_password:
+        parameters.pop('MasterUserPassword', None)
+    if cloudwatch_logs_enabled:
+        parameters['CloudwatchLogsExportConfiguration'] = cloudwatch_logs_enabled
+    if not module.params['storage_type']:
+        parameters.pop('Iops', None)
+
+    instance = get_instance(client, module, instance_id)
+    updated_parameters = get_changing_options_with_inconsistent_keys(parameters, instance, purge_cloudwatch_logs, purge_security_groups)
+    updated_parameters.update(get_changing_options_with_consistent_keys(parameters, instance))
+    parameters = updated_parameters
+
+    if instance.get('StorageType') == 'io1':
+        # Bundle Iops and AllocatedStorage while updating io1 RDS Instance
+        current_iops = instance.get('PendingModifiedValues', {}).get('Iops', instance['Iops'])
+        current_allocated_storage = instance.get('PendingModifiedValues', {}).get('AllocatedStorage', instance['AllocatedStorage'])
+        new_iops = module.params.get('iops')
+        new_allocated_storage = module.params.get('allocated_storage')
+
+        if current_iops != new_iops or current_allocated_storage != new_allocated_storage:
+            parameters['AllocatedStorage'] = new_allocated_storage
+            parameters['Iops'] = new_iops
+
+    if parameters.get('NewDBInstanceIdentifier') and instance.get('PendingModifiedValues', {}).get('DBInstanceIdentifier'):
+        if parameters['NewDBInstanceIdentifier'] == instance['PendingModifiedValues']['DBInstanceIdentifier'] and not apply_immediately:
+            parameters.pop('NewDBInstanceIdentifier')
+
+    if parameters:
+        parameters['DBInstanceIdentifier'] = instance_id
+        if apply_immediately is not None:
+            parameters['ApplyImmediately'] = apply_immediately
+
+    return parameters
+
+
+def get_current_attributes_with_inconsistent_keys(instance):
+    options = {}
+    if instance.get('PendingModifiedValues', {}).get('PendingCloudwatchLogsExports', {}).get('LogTypesToEnable', []):
+        current_enabled = instance['PendingModifiedValues']['PendingCloudwatchLogsExports']['LogTypesToEnable']
+        current_disabled = instance['PendingModifiedValues']['PendingCloudwatchLogsExports']['LogTypesToDisable']
+        options['CloudwatchLogsExportConfiguration'] = {'LogTypesToEnable': current_enabled, 'LogTypesToDisable': current_disabled}
+    else:
+        options['CloudwatchLogsExportConfiguration'] = {'LogTypesToEnable': instance.get('EnabledCloudwatchLogsExports', []), 'LogTypesToDisable': []}
+    if instance.get('PendingModifiedValues', {}).get('Port'):
+        options['DBPortNumber'] = instance['PendingModifiedValues']['Port']
+    else:
+        options['DBPortNumber'] = instance['Endpoint']['Port']
+    if instance.get('PendingModifiedValues', {}).get('DBSubnetGroupName'):
+        options['DBSubnetGroupName'] = instance['PendingModifiedValues']['DBSubnetGroupName']
+    else:
+        options['DBSubnetGroupName'] = instance['DBSubnetGroup']['DBSubnetGroupName']
+    if instance.get('PendingModifiedValues', {}).get('ProcessorFeatures'):
+        options['ProcessorFeatures'] = instance['PendingModifiedValues']['ProcessorFeatures']
+    else:
+        options['ProcessorFeatures'] = instance.get('ProcessorFeatures', {})
+    options['OptionGroupName'] = [g['OptionGroupName'] for g in instance['OptionGroupMemberships']]
+    options['DBSecurityGroups'] = [sg['DBSecurityGroupName'] for sg in instance['DBSecurityGroups'] if sg['Status'] in ['adding', 'active']]
+    options['VpcSecurityGroupIds'] = [sg['VpcSecurityGroupId'] for sg in instance['VpcSecurityGroups'] if sg['Status'] in ['adding', 'active']]
+    options['DBParameterGroupName'] = [parameter_group['DBParameterGroupName'] for parameter_group in instance['DBParameterGroups']]
+    options['EnableIAMDatabaseAuthentication'] = instance['IAMDatabaseAuthenticationEnabled']
+    # PerformanceInsightsEnabled is not returned on older RDS instances it seems
+    options['EnablePerformanceInsights'] = instance.get('PerformanceInsightsEnabled', False)
+    options['NewDBInstanceIdentifier'] = instance['DBInstanceIdentifier']
+
+    # Neither of these are returned via describe_db_instances, so if either is specified during a check_mode run, changed=True
+    options['AllowMajorVersionUpgrade'] = None
+    options['MasterUserPassword'] = None
+
+    return options
+
+
+def get_changing_options_with_inconsistent_keys(modify_params, instance, purge_cloudwatch_logs, purge_security_groups):
+    changing_params = {}
+    current_options = get_current_attributes_with_inconsistent_keys(instance)
+    for option in current_options:
+        current_option = current_options[option]
+        desired_option = modify_params.pop(option, None)
+        if desired_option is None:
+            continue
+
+        # TODO: allow other purge_option module parameters rather than just checking for things to add
+        if isinstance(current_option, list):
+            if isinstance(desired_option, list):
+                if (
+                    set(desired_option) < set(current_option) and
+                    option in ('DBSecurityGroups', 'VpcSecurityGroupIds',) and purge_security_groups
+                ):
+                    changing_params[option] = desired_option
+                elif set(desired_option) <= set(current_option):
+                    continue
+            elif isinstance(desired_option, string_types):
+                if desired_option in current_option:
+                    continue
+
+        # Current option and desired option are the same - continue loop
+        if option != 'ProcessorFeatures' and current_option == desired_option:
+            continue
+
+        if option == 'ProcessorFeatures' and current_option == boto3_tag_list_to_ansible_dict(desired_option, 'Name', 'Value'):
+            continue
+
+        # Current option and desired option are different - add to changing_params list
+        if option == 'ProcessorFeatures' and desired_option == []:
+            changing_params['UseDefaultProcessorFeatures'] = True
+        elif option == 'CloudwatchLogsExportConfiguration':
+            current_option = set(current_option.get('LogTypesToEnable', []))
+            desired_option = set(desired_option)
+            format_option = {'EnableLogTypes': [], 'DisableLogTypes': []}
+            format_option['EnableLogTypes'] = list(desired_option.difference(current_option))
+            if purge_cloudwatch_logs:
+                format_option['DisableLogTypes'] = list(current_option.difference(desired_option))
+            if format_option['EnableLogTypes'] or format_option['DisableLogTypes']:
+                changing_params[option] = format_option
+        elif option in ('DBSecurityGroups', 'VpcSecurityGroupIds',):
+            if purge_security_groups:
+                changing_params[option] = desired_option
+            else:
+                changing_params[option] = list(set(current_option) | set(desired_option))
+        else:
+            changing_params[option] = desired_option
+
+    return changing_params
+
+
+def get_changing_options_with_consistent_keys(modify_params, instance):
+    changing_params = {}
+
+    for param in modify_params:
+        current_option = instance.get('PendingModifiedValues', {}).get(param, None)
+        if current_option is None:
+            current_option = instance.get(param, None)
+        if modify_params[param] != current_option:
+            changing_params[param] = modify_params[param]
+
+    return changing_params
+
+
+def validate_options(client, module, instance):
+    state = module.params['state']
+    skip_final_snapshot = module.params['skip_final_snapshot']
+    snapshot_id = module.params['final_db_snapshot_identifier']
+    modified_id = module.params['new_db_instance_identifier']
+    engine = module.params['engine']
+    tde_options = bool(module.params['tde_credential_password'] or module.params['tde_credential_arn'])
+    read_replica = module.params['read_replica']
+    creation_source = module.params['creation_source']
+    source_instance = module.params['source_db_instance_identifier']
+    if module.params['source_region'] is not None:
+        same_region = bool(module.params['source_region'] == module.params['region'])
+    else:
+        same_region = True
+
+    if modified_id:
+        modified_instance = get_instance(client, module, modified_id)
+    else:
+        modified_instance = {}
+
+    if modified_id and instance and modified_instance:
+        module.fail_json(msg='A new instance ID {0} was provided but it already exists'.format(modified_id))
+    if modified_id and not instance and modified_instance:
+        module.fail_json(msg='A new instance ID {0} was provided but the instance to be renamed does not exist'.format(modified_id))
+    if state in ('absent', 'terminated') and instance and not skip_final_snapshot and snapshot_id is None:
+        module.fail_json(msg='skip_final_snapshot is false but all of the following are missing: final_db_snapshot_identifier')
+    if engine is not None and not (engine.startswith('mysql') or engine.startswith('oracle')) and tde_options:
+        module.fail_json(msg='TDE is available for MySQL and Oracle DB instances')
+    if read_replica is True and not instance and creation_source not in [None, 'instance']:
+        module.fail_json(msg='Cannot create a read replica from {0}. You must use a source DB instance'.format(creation_source))
+    if read_replica is True and not instance and not source_instance:
+        module.fail_json(msg='read_replica is true and the instance does not exist yet but all of the following are missing: source_db_instance_identifier')
+
+
+def update_instance(client, module, instance, instance_id):
+    changed = False
+
+    # Get newly created DB instance
+    if not instance:
+        instance = get_instance(client, module, instance_id)
+
+    # Check tagging/promoting/rebooting/starting/stopping instance
+    changed |= ensure_tags(
+        client, module, instance['DBInstanceArn'], instance['Tags'], module.params['tags'], module.params['purge_tags']
+    )
+    changed |= promote_replication_instance(client, module, instance, module.params['read_replica'])
+    changed |= update_instance_state(client, module, instance, module.params['state'])
+
+    return changed
+
+
+def promote_replication_instance(client, module, instance, read_replica):
+    changed = False
+    if read_replica is False:
+        # 'StatusInfos' only exists when the instance is a read replica
+        # See https://awscli.amazonaws.com/v2/documentation/api/latest/reference/rds/describe-db-instances.html
+        if bool(instance.get('StatusInfos')):
+            try:
+                result, changed = call_method(client, module, method_name='promote_read_replica',
+                                              parameters={'DBInstanceIdentifier': instance['DBInstanceIdentifier']})
+            except is_boto3_error_message('DB Instance is not a read replica'):
+                pass
+    return changed
+
+
+def ensure_iam_roles(client, module, instance_id):
+    '''
+    Ensure specified IAM roles are associated with DB instance
+
+        Parameters:
+            client: RDS client
+            module: AWSModule
+            instance_id: DB's instance ID
+
+        Returns:
+            changed (bool): True if changes were successfully made to DB instance's IAM roles; False if not
+    '''
+    instance = camel_dict_to_snake_dict(get_instance(client, module, instance_id), ignore_list=['Tags', 'ProcessorFeatures'])
+
+    # Ensure engine type supports associating IAM roles
+    engine = instance.get('engine')
+    if engine not in valid_engines_iam_roles:
+        module.fail_json(msg='DB engine {0} is not valid for adding IAM roles. Valid engines are {1}'.format(engine, valid_engines_iam_roles))
+
+    changed = False
+    purge_iam_roles = module.params.get('purge_iam_roles')
+    target_roles = module.params.get('iam_roles') if module.params.get('iam_roles') else []
+    existing_roles = instance.get('associated_roles', [])
+    roles_to_add, roles_to_remove = compare_iam_roles(existing_roles, target_roles, purge_iam_roles)
+    if bool(roles_to_add or roles_to_remove):
+        changed = True
+        # Don't update on check_mode
+        if module.check_mode:
+            module.exit_json(changed=changed, **instance)
+        else:
+            update_iam_roles(client, module, instance_id, roles_to_add, roles_to_remove)
+    return changed
+
+
+def update_instance_state(client, module, instance, state):
+    changed = False
+    if state in ['rebooted', 'restarted']:
+        changed |= reboot_running_db_instance(client, module, instance)
+    if state in ['started', 'running', 'stopped']:
+        changed |= start_or_stop_instance(client, module, instance, state)
+    return changed
+
+
+def reboot_running_db_instance(client, module, instance):
+    parameters = {'DBInstanceIdentifier': instance['DBInstanceIdentifier']}
+    if instance['DBInstanceStatus'] in ['stopped', 'stopping']:
+        call_method(client, module, 'start_db_instance', parameters)
+    if module.params.get('force_failover') is not None:
+        parameters['ForceFailover'] = module.params['force_failover']
+    results, changed = call_method(client, module, 'reboot_db_instance', parameters)
+    return changed
+
+
+def start_or_stop_instance(client, module, instance, state):
+    changed = False
+    parameters = {'DBInstanceIdentifier': instance['DBInstanceIdentifier']}
+    if state == 'stopped' and instance['DBInstanceStatus'] not in ['stopping', 'stopped']:
+        if module.params['db_snapshot_identifier']:
+            parameters['DBSnapshotIdentifier'] = module.params['db_snapshot_identifier']
+        result, changed = call_method(client, module, 'stop_db_instance', parameters)
+    elif state == 'started' and instance['DBInstanceStatus'] not in ['available', 'starting', 'restarting']:
+        result, changed = call_method(client, module, 'start_db_instance', parameters)
+    return changed
+
+
+def main():
+    arg_spec = dict(
+        state=dict(choices=['present', 'absent', 'terminated', 'running', 'started', 'stopped', 'rebooted', 'restarted'], default='present'),
+        creation_source=dict(choices=['snapshot', 's3', 'instance']),
+        force_update_password=dict(type='bool', default=False, no_log=False),
+        purge_cloudwatch_logs_exports=dict(type='bool', default=True),
+        purge_iam_roles=dict(type='bool', default=False),
+        purge_tags=dict(type='bool', default=True),
+        read_replica=dict(type='bool'),
+        wait=dict(type='bool', default=True),
+        purge_security_groups=dict(type='bool', default=True),
+    )
+
+    parameter_options = dict(
+        allocated_storage=dict(type='int'),
+        allow_major_version_upgrade=dict(type='bool'),
+        apply_immediately=dict(type='bool', default=False),
+        auto_minor_version_upgrade=dict(type='bool'),
+        availability_zone=dict(aliases=['az', 'zone']),
+        backup_retention_period=dict(type='int'),
+        ca_certificate_identifier=dict(),
+        character_set_name=dict(),
+        copy_tags_to_snapshot=dict(type='bool'),
+        db_cluster_identifier=dict(aliases=['cluster_id']),
+        db_instance_class=dict(aliases=['class', 'instance_type']),
+        db_instance_identifier=dict(required=True, aliases=['instance_id', 'id']),
+        db_name=dict(),
+        db_parameter_group_name=dict(),
+        db_security_groups=dict(type='list', elements='str'),
+        db_snapshot_identifier=dict(type='str', aliases=['snapshot_identifier', 'snapshot_id']),
+        db_subnet_group_name=dict(aliases=['subnet_group']),
+        deletion_protection=dict(type='bool'),
+        domain=dict(),
+        domain_iam_role_name=dict(),
+        enable_cloudwatch_logs_exports=dict(type='list', aliases=['cloudwatch_log_exports'], elements='str'),
+        enable_iam_database_authentication=dict(type='bool'),
+        enable_performance_insights=dict(type='bool'),
+        engine=dict(type='str', choices=valid_engines),
+        engine_version=dict(),
+        final_db_snapshot_identifier=dict(aliases=['final_snapshot_identifier']),
+        force_failover=dict(type='bool'),
+        iam_roles=dict(type='list', elements='dict'),
+        iops=dict(type='int'),
+        kms_key_id=dict(),
+        license_model=dict(),
+        master_user_password=dict(aliases=['password'], no_log=True),
+        master_username=dict(aliases=['username']),
+        max_allocated_storage=dict(type='int'),
+        monitoring_interval=dict(type='int'),
+        monitoring_role_arn=dict(),
+        multi_az=dict(type='bool'),
+        new_db_instance_identifier=dict(aliases=['new_instance_id', 'new_id']),
+        option_group_name=dict(),
+        performance_insights_kms_key_id=dict(),
+        performance_insights_retention_period=dict(type='int'),
+        port=dict(type='int'),
+        preferred_backup_window=dict(aliases=['backup_window']),
+        preferred_maintenance_window=dict(aliases=['maintenance_window']),
+        processor_features=dict(type='dict'),
+        promotion_tier=dict(),
+        publicly_accessible=dict(type='bool'),
+        restore_time=dict(),
+        s3_bucket_name=dict(),
+        s3_ingestion_role_arn=dict(),
+        s3_prefix=dict(),
+        skip_final_snapshot=dict(type='bool', default=False),
+        source_db_instance_identifier=dict(),
+        source_engine=dict(choices=['mysql']),
+        source_engine_version=dict(),
+        source_region=dict(),
+        storage_encrypted=dict(type='bool'),
+        storage_type=dict(choices=['standard', 'gp2', 'io1']),
+        tags=dict(type='dict', aliases=['resource_tags']),
+        tde_credential_arn=dict(aliases=['transparent_data_encryption_arn']),
+        tde_credential_password=dict(no_log=True, aliases=['transparent_data_encryption_password']),
+        timezone=dict(),
+        use_latest_restorable_time=dict(type='bool', aliases=['restore_from_latest']),
+        vpc_security_group_ids=dict(type='list', elements='str')
+    )
+    arg_spec.update(parameter_options)
+
+    required_if = [
+        ('engine', 'aurora', ('db_cluster_identifier',)),
+        ('engine', 'aurora-mysql', ('db_cluster_identifier',)),
+        ('engine', 'aurora-postresql', ('db_cluster_identifier',)),
+        ('storage_type', 'io1', ('iops', 'allocated_storage')),
+        ('creation_source', 'snapshot', ('db_snapshot_identifier', 'engine')),
+        ('creation_source', 's3', (
+            's3_bucket_name', 'engine', 'master_username', 'master_user_password',
+            'source_engine', 'source_engine_version', 's3_ingestion_role_arn')),
+    ]
+    mutually_exclusive = [
+        ('s3_bucket_name', 'source_db_instance_identifier', 'db_snapshot_identifier'),
+        ('use_latest_restorable_time', 'restore_time'),
+        ('availability_zone', 'multi_az'),
+    ]
+
+    module = AnsibleAWSModule(
+        argument_spec=arg_spec,
+        required_if=required_if,
+        mutually_exclusive=mutually_exclusive,
+        supports_check_mode=True
+    )
+
+    # Sanitize instance identifiers
+    module.params['db_instance_identifier'] = module.params['db_instance_identifier'].lower()
+    if module.params['new_db_instance_identifier']:
+        module.params['new_db_instance_identifier'] = module.params['new_db_instance_identifier'].lower()
+
+    # Sanitize processor features
+    if module.params['processor_features'] is not None:
+        module.params['processor_features'] = dict((k, to_text(v)) for k, v in module.params['processor_features'].items())
+
+    # Ensure dates are in lowercase
+    if module.params['preferred_maintenance_window']:
+        module.params['preferred_maintenance_window'] = module.params['preferred_maintenance_window'].lower()
+
+    # Throw warning regarding case when allow_major_version_upgrade is specified in check_mode
+    # describe_rds_instance never returns this value, so on check_mode, it will always return changed=True
+    # In non-check mode runs, changed will return the correct value, so no need to warn there.
+    # see: amazon.aws.module_util.rds.handle_errors.
+    if module.params.get('allow_major_version_upgrade') and module.check_mode:
+        module.warn('allow_major_version_upgrade is not returned when describing db instances, so changed will always be `True` on check mode runs.')
+
+    client = module.client('rds')
+    changed = False
+    state = module.params['state']
+    instance_id = module.params['db_instance_identifier']
+    instance = get_instance(client, module, instance_id)
+    validate_options(client, module, instance)
+    method_name = get_rds_method_attribute_name(instance, state, module.params['creation_source'], module.params['read_replica'])
+
+    if method_name:
+
+        # Exit on create/delete if check_mode
+        if module.check_mode and method_name in ['create_db_instance', 'delete_db_instance']:
+            module.exit_json(changed=True, **camel_dict_to_snake_dict(instance, ignore_list=['Tags', 'ProcessorFeatures']))
+
+        raw_parameters = arg_spec_to_rds_params(dict((k, module.params[k]) for k in module.params if k in parameter_options))
+        parameters_to_modify = get_parameters(client, module, raw_parameters, method_name)
+
+        if parameters_to_modify:
+            # Exit on check_mode when parameters to modify
+            if module.check_mode:
+                module.exit_json(changed=True, **camel_dict_to_snake_dict(instance, ignore_list=['Tags', 'ProcessorFeatures']))
+            result, changed = call_method(client, module, method_name, parameters_to_modify)
+
+        instance_id = get_final_identifier(method_name, module)
+
+        if state != 'absent':
+            # Check tagging/promoting/rebooting/starting/stopping instance
+            if not module.check_mode or instance:
+                changed |= update_instance(client, module, instance, instance_id)
+
+            # Check IAM roles
+            if module.params.get('iam_roles') or module.params.get('purge_iam_roles'):
+                changed |= ensure_iam_roles(client, module, instance_id)
+
+        if changed:
+            instance = get_instance(client, module, instance_id)
+            if state != 'absent' and (instance or not module.check_mode):
+                for attempt_to_wait in range(0, 10):
+                    instance = get_instance(client, module, instance_id)
+                    if instance:
+                        break
+                    else:
+                        sleep(5)
+
+        if state == 'absent' and changed and not module.params['skip_final_snapshot']:
+            instance.update(FinalSnapshot=get_final_snapshot(client, module, module.params['final_db_snapshot_identifier']))
+
+    pending_processor_features = None
+    if instance.get('PendingModifiedValues', {}).get('ProcessorFeatures'):
+        pending_processor_features = instance['PendingModifiedValues'].pop('ProcessorFeatures')
+    instance = camel_dict_to_snake_dict(instance, ignore_list=['Tags', 'ProcessorFeatures'])
+    if pending_processor_features is not None:
+        instance['pending_modified_values']['processor_features'] = pending_processor_features
+
+    module.exit_json(changed=changed, **instance)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/plugins/modules/rds_instance_info.py b/plugins/modules/rds_instance_info.py
new file mode 100644
--- /dev/null
+++ b/plugins/modules/rds_instance_info.py
@@ -0,0 +1,421 @@
+#!/usr/bin/python
+# Copyright (c) 2017, 2018 Michael De La Rue
+# Copyright (c) 2017, 2018 Will Thames
+# Copyright (c) 2017 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: rds_instance_info
+version_added: 1.0.0
+short_description: obtain information about one or more RDS instances
+description:
+  - Obtain information about one or more RDS instances.
+options:
+  db_instance_identifier:
+    description:
+      - The RDS instance's unique identifier.
+    required: false
+    aliases:
+      - id
+    type: str
+  filters:
+    description:
+      - A filter that specifies one or more DB instances to describe.
+        See U(https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_DescribeDBInstances.html)
+    type: dict
+author:
+    - "Will Thames (@willthames)"
+    - "Michael De La Rue (@mikedlr)"
+extends_documentation_fragment:
+- amazon.aws.aws
+- amazon.aws.ec2
+
+'''
+
+EXAMPLES = '''
+- name: Get information about an instance
+  amazon.aws.rds_instance_info:
+    db_instance_identifier: new-database
+  register: new_database_info
+
+- name: Get all RDS instances
+  amazon.aws.rds_instance_info:
+'''
+
+RETURN = '''
+instances:
+  description: List of RDS instances
+  returned: always
+  type: complex
+  contains:
+    allocated_storage:
+      description: Gigabytes of storage allocated to the database
+      returned: always
+      type: int
+      sample: 10
+    auto_minor_version_upgrade:
+      description: Whether minor version upgrades happen automatically
+      returned: always
+      type: bool
+      sample: true
+    availability_zone:
+      description: Availability Zone in which the database resides
+      returned: always
+      type: str
+      sample: us-west-2b
+    backup_retention_period:
+      description: Days for which backups are retained
+      returned: always
+      type: int
+      sample: 7
+    ca_certificate_identifier:
+      description: ID for the CA certificate
+      returned: always
+      type: str
+      sample: rds-ca-2015
+    copy_tags_to_snapshot:
+      description: Whether DB tags should be copied to the snapshot
+      returned: always
+      type: bool
+      sample: false
+    db_instance_arn:
+      description: ARN of the database instance
+      returned: always
+      type: str
+      sample: arn:aws:rds:us-west-2:111111111111:db:helloworld-rds
+    db_instance_class:
+      description: Instance class of the database instance
+      returned: always
+      type: str
+      sample: db.t2.small
+    db_instance_identifier:
+      description: Database instance identifier
+      returned: always
+      type: str
+      sample: helloworld-rds
+    db_instance_port:
+      description: Port used by the database instance
+      returned: always
+      type: int
+      sample: 0
+    db_instance_status:
+      description: Status of the database instance
+      returned: always
+      type: str
+      sample: available
+    db_name:
+      description: Name of the database
+      returned: always
+      type: str
+      sample: management
+    db_parameter_groups:
+      description: List of database parameter groups
+      returned: always
+      type: complex
+      contains:
+        db_parameter_group_name:
+          description: Name of the database parameter group
+          returned: always
+          type: str
+          sample: psql-pg-helloworld
+        parameter_apply_status:
+          description: Whether the parameter group has been applied
+          returned: always
+          type: str
+          sample: in-sync
+    db_security_groups:
+      description: List of security groups used by the database instance
+      returned: always
+      type: list
+      sample: []
+    db_subnet_group:
+      description: list of subnet groups
+      returned: always
+      type: complex
+      contains:
+        db_subnet_group_description:
+          description: Description of the DB subnet group
+          returned: always
+          type: str
+          sample: My database subnet group
+        db_subnet_group_name:
+          description: Name of the database subnet group
+          returned: always
+          type: str
+          sample: my-subnet-group
+        subnet_group_status:
+          description: Subnet group status
+          returned: always
+          type: str
+          sample: Complete
+        subnets:
+          description: List of subnets in the subnet group
+          returned: always
+          type: complex
+          contains:
+            subnet_availability_zone:
+              description: Availability zone of the subnet
+              returned: always
+              type: complex
+              contains:
+                name:
+                  description: Name of the availability zone
+                  returned: always
+                  type: str
+                  sample: us-west-2c
+            subnet_identifier:
+              description: Subnet ID
+              returned: always
+              type: str
+              sample: subnet-abcd1234
+            subnet_status:
+              description: Subnet status
+              returned: always
+              type: str
+              sample: Active
+        vpc_id:
+          description: VPC id of the subnet group
+          returned: always
+          type: str
+          sample: vpc-abcd1234
+    dbi_resource_id:
+      description: AWS Region-unique, immutable identifier for the DB instance
+      returned: always
+      type: str
+      sample: db-AAAAAAAAAAAAAAAAAAAAAAAAAA
+    deletion_protection:
+      description: C(True) if the DB instance has deletion protection enabled, C(False) if not.
+      returned: always
+      type: bool
+      sample: False
+      version_added: 3.3.0
+    domain_memberships:
+      description: List of domain memberships
+      returned: always
+      type: list
+      sample: []
+    endpoint:
+      description: Database endpoint
+      returned: always
+      type: complex
+      contains:
+        address:
+          description: Database endpoint address
+          returned: always
+          type: str
+          sample: helloworld-rds.ctrqpe3so1sf.us-west-2.rds.amazonaws.com
+        hosted_zone_id:
+          description: Route53 hosted zone ID
+          returned: always
+          type: str
+          sample: Z1PABCD0000000
+        port:
+          description: Database endpoint port
+          returned: always
+          type: int
+          sample: 5432
+    engine:
+      description: Database engine
+      returned: always
+      type: str
+      sample: postgres
+    engine_version:
+      description: Database engine version
+      returned: always
+      type: str
+      sample: 9.5.10
+    iam_database_authentication_enabled:
+      description: Whether database authentication through IAM is enabled
+      returned: always
+      type: bool
+      sample: false
+    instance_create_time:
+      description: Date and time the instance was created
+      returned: always
+      type: str
+      sample: '2017-10-10T04:00:07.434000+00:00'
+    iops:
+      description: The Provisioned IOPS value for the DB instance.
+      returned: always
+      type: int
+      sample: 1000
+    kms_key_id:
+      description: KMS Key ID
+      returned: always
+      type: str
+      sample: arn:aws:kms:us-west-2:111111111111:key/abcd1234-0000-abcd-1111-0123456789ab
+    latest_restorable_time:
+      description: Latest time to which a database can be restored with point-in-time restore
+      returned: always
+      type: str
+      sample: '2018-05-17T00:03:56+00:00'
+    license_model:
+      description: License model
+      returned: always
+      type: str
+      sample: postgresql-license
+    master_username:
+      description: Database master username
+      returned: always
+      type: str
+      sample: dbadmin
+    monitoring_interval:
+      description: Interval, in seconds, between points when Enhanced Monitoring metrics are collected for the DB instance
+      returned: always
+      type: int
+      sample: 0
+    multi_az:
+      description: Whether Multi-AZ is on
+      returned: always
+      type: bool
+      sample: false
+    option_group_memberships:
+      description: List of option groups
+      returned: always
+      type: complex
+      contains:
+        option_group_name:
+          description: Option group name
+          returned: always
+          type: str
+          sample: default:postgres-9-5
+        status:
+          description: Status of option group
+          returned: always
+          type: str
+          sample: in-sync
+    pending_modified_values:
+      description: Modified values pending application
+      returned: always
+      type: complex
+      contains: {}
+    performance_insights_enabled:
+      description: Whether performance insights are enabled
+      returned: always
+      type: bool
+      sample: false
+    preferred_backup_window:
+      description: Preferred backup window
+      returned: always
+      type: str
+      sample: 04:00-05:00
+    preferred_maintenance_window:
+      description: Preferred maintenance window
+      returned: always
+      type: str
+      sample: mon:05:00-mon:05:30
+    publicly_accessible:
+      description: Whether the DB is publicly accessible
+      returned: always
+      type: bool
+      sample: false
+    read_replica_db_instance_identifiers:
+      description: List of database instance read replicas
+      returned: always
+      type: list
+      sample: []
+    storage_encrypted:
+      description: Whether the storage is encrypted
+      returned: always
+      type: bool
+      sample: true
+    storage_type:
+      description: Storage type of the Database instance
+      returned: always
+      type: str
+      sample: gp2
+    tags:
+      description: Tags used by the database instance
+      returned: always
+      type: complex
+      contains: {}
+    vpc_security_groups:
+      description: List of VPC security groups
+      returned: always
+      type: complex
+      contains:
+        status:
+          description: Status of the VPC security group
+          returned: always
+          type: str
+          sample: active
+        vpc_security_group_id:
+          description: VPC Security Group ID
+          returned: always
+          type: str
+          sample: sg-abcd1234
+'''
+
+from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule, is_boto3_error_code
+from ansible_collections.amazon.aws.plugins.module_utils.ec2 import (ansible_dict_to_boto3_filter_list,
+                                                                     boto3_tag_list_to_ansible_dict,
+                                                                     AWSRetry,
+                                                                     camel_dict_to_snake_dict,
+                                                                     )
+
+
+try:
+    import botocore
+except ImportError:
+    pass  # handled by AnsibleAWSModule
+
+
+@AWSRetry.jittered_backoff()
+def _describe_db_instances(conn, **params):
+    paginator = conn.get_paginator('describe_db_instances')
+    try:
+        results = paginator.paginate(**params).build_full_result()['DBInstances']
+    except is_boto3_error_code('DBInstanceNotFound'):
+        results = []
+
+    return results
+
+
+def instance_info(module, conn):
+    instance_name = module.params.get('db_instance_identifier')
+    filters = module.params.get('filters')
+
+    params = dict()
+    if instance_name:
+        params['DBInstanceIdentifier'] = instance_name
+    if filters:
+        params['Filters'] = ansible_dict_to_boto3_filter_list(filters)
+
+    try:
+        results = _describe_db_instances(conn, **params)
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, "Couldn't get instance information")
+
+    for instance in results:
+        try:
+            instance['Tags'] = boto3_tag_list_to_ansible_dict(conn.list_tags_for_resource(ResourceName=instance['DBInstanceArn'],
+                                                                                          aws_retry=True)['TagList'])
+        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+            module.fail_json_aws(e, "Couldn't get tags for instance %s" % instance['DBInstanceIdentifier'])
+
+    return dict(changed=False, instances=[camel_dict_to_snake_dict(instance, ignore_list=['Tags']) for instance in results])
+
+
+def main():
+    argument_spec = dict(
+        db_instance_identifier=dict(aliases=['id']),
+        filters=dict(type='dict')
+    )
+
+    module = AnsibleAWSModule(
+        argument_spec=argument_spec,
+        supports_check_mode=True,
+    )
+
+    conn = module.client('rds', retry_decorator=AWSRetry.jittered_backoff(retries=10))
+
+    module.exit_json(**instance_info(module, conn))
+
+
+if __name__ == '__main__':
+    main()
diff --git a/plugins/modules/rds_instance_snapshot.py b/plugins/modules/rds_instance_snapshot.py
new file mode 100644
--- /dev/null
+++ b/plugins/modules/rds_instance_snapshot.py
@@ -0,0 +1,380 @@
+#!/usr/bin/python
+# Copyright (c) 2014 Ansible Project
+# Copyright (c) 2017, 2018, 2019 Will Thames
+# Copyright (c) 2017, 2018 Michael De La Rue
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: rds_instance_snapshot
+version_added: 1.0.0
+short_description: Manage Amazon RDS instance snapshots
+description:
+  - Creates or deletes RDS snapshots.
+options:
+  state:
+    description:
+      - Specify the desired state of the snapshot.
+    default: present
+    choices: [ 'present', 'absent']
+    type: str
+  db_snapshot_identifier:
+    description:
+      - The snapshot to manage.
+    required: true
+    aliases:
+      - id
+      - snapshot_id
+    type: str
+  db_instance_identifier:
+    description:
+      - Database instance identifier. Required when creating a snapshot.
+    aliases:
+      - instance_id
+    type: str
+  source_db_snapshot_identifier:
+    description:
+      - The identifier of the source DB snapshot.
+      - Required when copying a snapshot.
+      - If the source snapshot is in the same AWS region as the copy, specify the snapshot's identifier.
+      - If the source snapshot is in a different AWS region as the copy, specify the snapshot's ARN.
+    aliases:
+      - source_id
+      - source_snapshot_id
+    type: str
+    version_added: 3.3.0
+  source_region:
+    description:
+      - The region that contains the snapshot to be copied.
+    type: str
+    version_added: 3.3.0
+  copy_tags:
+    description:
+      - Whether to copy all tags from I(source_db_snapshot_identifier) to I(db_instance_identifier).
+    type: bool
+    default: False
+    version_added: 3.3.0
+  wait:
+    description:
+      - Whether or not to wait for snapshot creation or deletion.
+    type: bool
+    default: False
+  wait_timeout:
+    description:
+      - how long before wait gives up, in seconds.
+    default: 300
+    type: int
+author:
+  - "Will Thames (@willthames)"
+  - "Michael De La Rue (@mikedlr)"
+  - "Alina Buzachis (@alinabuzachis)"
+  - "Joseph Torcasso (@jatorcasso)"
+extends_documentation_fragment:
+  - amazon.aws.aws
+  - amazon.aws.ec2
+  - amazon.aws.tags
+'''
+
+EXAMPLES = r'''
+- name: Create snapshot
+  amazon.aws.rds_instance_snapshot:
+    db_instance_identifier: new-database
+    db_snapshot_identifier: new-database-snapshot
+  register: snapshot
+
+- name: Copy snapshot from a different region and copy its tags
+  amazon.aws.rds_instance_snapshot:
+    id: new-database-snapshot-copy
+    region: us-east-1
+    source_id: "{{ snapshot.db_snapshot_arn }}"
+    source_region: us-east-2
+    copy_tags: true
+
+- name: Delete snapshot
+  amazon.aws.rds_instance_snapshot:
+    db_snapshot_identifier: new-database-snapshot
+    state: absent
+'''
+
+RETURN = r'''
+allocated_storage:
+  description: How much storage is allocated in GB.
+  returned: always
+  type: int
+  sample: 20
+availability_zone:
+  description: Availability zone of the database from which the snapshot was created.
+  returned: always
+  type: str
+  sample: us-west-2a
+db_instance_identifier:
+  description: Database from which the snapshot was created.
+  returned: always
+  type: str
+  sample: ansible-test-16638696
+db_snapshot_arn:
+  description: Amazon Resource Name for the snapshot.
+  returned: always
+  type: str
+  sample: arn:aws:rds:us-west-2:123456789012:snapshot:ansible-test-16638696-test-snapshot
+db_snapshot_identifier:
+  description: Name of the snapshot.
+  returned: always
+  type: str
+  sample: ansible-test-16638696-test-snapshot
+dbi_resource_id:
+  description: The identifier for the source DB instance, which can't be changed and which is unique to an AWS Region.
+  returned: always
+  type: str
+  sample: db-MM4P2U35RQRAMWD3QDOXWPZP4U
+encrypted:
+  description: Whether the snapshot is encrypted.
+  returned: always
+  type: bool
+  sample: false
+engine:
+  description: Engine of the database from which the snapshot was created.
+  returned: always
+  type: str
+  sample: mariadb
+engine_version:
+  description: Version of the database from which the snapshot was created.
+  returned: always
+  type: str
+  sample: 10.2.21
+iam_database_authentication_enabled:
+  description: Whether IAM database authentication is enabled.
+  returned: always
+  type: bool
+  sample: false
+instance_create_time:
+  description: Creation time of the instance from which the snapshot was created.
+  returned: always
+  type: str
+  sample: '2019-06-15T10:15:56.221000+00:00'
+license_model:
+  description: License model of the database.
+  returned: always
+  type: str
+  sample: general-public-license
+master_username:
+  description: Master username of the database.
+  returned: always
+  type: str
+  sample: test
+option_group_name:
+  description: Option group of the database.
+  returned: always
+  type: str
+  sample: default:mariadb-10-2
+percent_progress:
+  description: How much progress has been made taking the snapshot. Will be 100 for an available snapshot.
+  returned: always
+  type: int
+  sample: 100
+port:
+  description: Port on which the database is listening.
+  returned: always
+  type: int
+  sample: 3306
+processor_features:
+  description: List of processor features of the database.
+  returned: always
+  type: list
+  sample: []
+source_db_snapshot_identifier:
+  description: The DB snapshot ARN that the DB snapshot was copied from.
+  returned: when snapshot is a copy
+  type: str
+  sample: arn:aws:rds:us-west-2:123456789012:snapshot:ansible-test-16638696-test-snapshot-source
+  version_added: 3.3.0
+snapshot_create_time:
+  description: Creation time of the snapshot.
+  returned: always
+  type: str
+  sample: '2019-06-15T10:46:23.776000+00:00'
+snapshot_type:
+  description: How the snapshot was created (always manual for this module!).
+  returned: always
+  type: str
+  sample: manual
+status:
+  description: Status of the snapshot.
+  returned: always
+  type: str
+  sample: available
+storage_type:
+  description: Storage type of the database.
+  returned: always
+  type: str
+  sample: gp2
+tags:
+  description: Tags applied to the snapshot.
+  returned: always
+  type: complex
+  contains: {}
+vpc_id:
+  description: ID of the VPC in which the DB lives.
+  returned: always
+  type: str
+  sample: vpc-09ff232e222710ae0
+'''
+
+try:
+    import botocore
+except ImportError:
+    pass  # protected by AnsibleAWSModule
+
+# import module snippets
+from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
+from ansible_collections.amazon.aws.plugins.module_utils.core import get_boto3_client_method_parameters
+from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code
+from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_tag_list
+from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry
+from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict
+from ansible_collections.amazon.aws.plugins.module_utils.rds import arg_spec_to_rds_params
+from ansible_collections.amazon.aws.plugins.module_utils.rds import call_method
+from ansible_collections.amazon.aws.plugins.module_utils.rds import ensure_tags
+from ansible_collections.amazon.aws.plugins.module_utils.rds import get_rds_method_attribute
+from ansible_collections.amazon.aws.plugins.module_utils.rds import get_tags
+
+
+def get_snapshot(snapshot_id):
+    try:
+        snapshot = client.describe_db_snapshots(DBSnapshotIdentifier=snapshot_id)['DBSnapshots'][0]
+        snapshot['Tags'] = get_tags(client, module, snapshot['DBSnapshotArn'])
+    except is_boto3_error_code("DBSnapshotNotFound"):
+        return {}
+    except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:  # pylint: disable=duplicate-except
+        module.fail_json_aws(e, msg="Couldn't get snapshot {0}".format(snapshot_id))
+    return snapshot
+
+
+def get_parameters(parameters, method_name):
+    if method_name == 'copy_db_snapshot':
+        parameters['TargetDBSnapshotIdentifier'] = module.params['db_snapshot_identifier']
+
+    required_options = get_boto3_client_method_parameters(client, method_name, required=True)
+    if any(parameters.get(k) is None for k in required_options):
+        module.fail_json(msg='To {0} requires the parameters: {1}'.format(
+            get_rds_method_attribute(method_name, module).operation_description, required_options))
+    options = get_boto3_client_method_parameters(client, method_name)
+    parameters = dict((k, v) for k, v in parameters.items() if k in options and v is not None)
+
+    return parameters
+
+
+def ensure_snapshot_absent():
+    snapshot_name = module.params.get("db_snapshot_identifier")
+    params = {"DBSnapshotIdentifier": snapshot_name}
+    changed = False
+
+    snapshot = get_snapshot(snapshot_name)
+    if not snapshot:
+        module.exit_json(changed=changed)
+    elif snapshot and snapshot["Status"] != "deleting":
+        snapshot, changed = call_method(client, module, "delete_db_snapshot", params)
+
+    module.exit_json(changed=changed)
+
+
+def ensure_snapshot_present(params):
+    source_id = module.params.get('source_db_snapshot_identifier')
+    snapshot_name = module.params.get('db_snapshot_identifier')
+    changed = False
+    snapshot = get_snapshot(snapshot_name)
+
+    # Copy snapshot
+    if source_id:
+        changed |= copy_snapshot(params)
+
+    # Create snapshot
+    elif not snapshot:
+        changed |= create_snapshot(params)
+
+    # Snapshot exists and we're not creating a copy - modify exising snapshot
+    else:
+        changed |= modify_snapshot()
+
+    snapshot = get_snapshot(snapshot_name)
+    module.exit_json(changed=changed, **camel_dict_to_snake_dict(snapshot, ignore_list=['Tags']))
+
+
+def create_snapshot(params):
+    method_params = get_parameters(params, 'create_db_snapshot')
+    if method_params.get('Tags'):
+        method_params['Tags'] = ansible_dict_to_boto3_tag_list(method_params['Tags'])
+    snapshot, changed = call_method(client, module, 'create_db_snapshot', method_params)
+
+    return changed
+
+
+def copy_snapshot(params):
+    changed = False
+    snapshot_id = module.params.get('db_snapshot_identifier')
+    snapshot = get_snapshot(snapshot_id)
+
+    if not snapshot:
+        method_params = get_parameters(params, 'copy_db_snapshot')
+        if method_params.get('Tags'):
+            method_params['Tags'] = ansible_dict_to_boto3_tag_list(method_params['Tags'])
+        result, changed = call_method(client, module, 'copy_db_snapshot', method_params)
+
+    return changed
+
+
+def modify_snapshot():
+    # TODO - add other modifications aside from purely tags
+    changed = False
+    snapshot_id = module.params.get('db_snapshot_identifier')
+    snapshot = get_snapshot(snapshot_id)
+
+    if module.params.get('tags'):
+        changed |= ensure_tags(client, module, snapshot['DBSnapshotArn'], snapshot['Tags'], module.params['tags'], module.params['purge_tags'])
+
+    return changed
+
+
+def main():
+    global client
+    global module
+
+    argument_spec = dict(
+        state=dict(choices=['present', 'absent'], default='present'),
+        db_snapshot_identifier=dict(aliases=['id', 'snapshot_id'], required=True),
+        db_instance_identifier=dict(aliases=['instance_id']),
+        source_db_snapshot_identifier=dict(aliases=['source_id', 'source_snapshot_id']),
+        wait=dict(type='bool', default=False),
+        wait_timeout=dict(type='int', default=300),
+        tags=dict(type='dict', aliases=['resource_tags']),
+        purge_tags=dict(type='bool', default=True),
+        copy_tags=dict(type='bool', default=False),
+        source_region=dict(type='str'),
+    )
+
+    module = AnsibleAWSModule(
+        argument_spec=argument_spec,
+        supports_check_mode=True
+    )
+
+    retry_decorator = AWSRetry.jittered_backoff(retries=10)
+    try:
+        client = module.client('rds', retry_decorator=retry_decorator)
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg="Failed to connect to AWS.")
+
+    state = module.params.get("state")
+    if state == 'absent':
+        ensure_snapshot_absent()
+
+    elif state == 'present':
+        params = arg_spec_to_rds_params(dict((k, module.params[k]) for k in module.params if k in argument_spec))
+        ensure_snapshot_present(params)
+
+
+if __name__ == '__main__':
+    main()


Test patch:
diff --git a/tests/integration/targets/rds_instance/aliases b/tests/integration/targets/rds_instance/aliases
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/aliases
@@ -0,0 +1,4 @@
+slow
+
+cloud/aws
+rds_instance_info
diff --git a/tests/integration/targets/rds_instance/inventory b/tests/integration/targets/rds_instance/inventory
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/inventory
@@ -0,0 +1,21 @@
+# inventory names shortened down to fit resource name length limits
+[tests]
+processor
+restore
+sgroups
+complex
+states
+modify
+tagging
+replica
+upgrade
+snapshot
+aurora
+
+# TODO: uncomment after adding iam:CreatePolicy and iam:DeletePolicy
+# iam_roles
+
+
+[all:vars]
+ansible_connection=local
+ansible_python_interpreter="{{ ansible_playbook_python }}"
diff --git a/tests/integration/targets/rds_instance/main.yml b/tests/integration/targets/rds_instance/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/main.yml
@@ -0,0 +1,9 @@
+# Beware: most of our tests here are run in parallel.
+# To add new tests you'll need to add a new host to the inventory and a matching
+# '{{ inventory_hostname }}'.yml file in roles/rds_instance/tasks/
+
+- hosts: all
+  gather_facts: no
+  strategy: free
+  roles:
+  - rds_instance
diff --git a/tests/integration/targets/rds_instance/meta/main.yml b/tests/integration/targets/rds_instance/meta/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/meta/main.yml
@@ -0,0 +1 @@
+dependencies: []
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/defaults/main.yml b/tests/integration/targets/rds_instance/roles/rds_instance/defaults/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/defaults/main.yml
@@ -0,0 +1,38 @@
+instance_id: ansible-test-{{ inventory_hostname | replace('_','-') }}{{ tiny_prefix
+  }}
+modified_instance_id: '{{ instance_id }}-updated'
+username: test
+password: test12345678
+db_instance_class: db.t3.micro
+storage_encrypted_db_instance_class: db.t3.small
+modified_db_instance_class: db.t3.medium
+allocated_storage: 20
+modified_allocated_storage: 30
+io1_allocated_storage: 100
+io1_modified_allocated_storage: 110
+monitoring_interval: 60
+preferred_maintenance_window: mon:06:20-mon:06:50
+storage_type: io1
+iops: 1000
+
+# For aurora tests
+cluster_id: '{{ instance_id }}-cluster'
+aurora_db_instance_class: db.t3.medium
+
+# For oracle tests
+# Smallest instance that permits modification of the coreCount
+oracle_ee_db_instance_class: db.r5.2xlarge
+modified_processor_features:
+  coreCount: 4
+  threadsPerCore: 2
+
+# For mariadb tests
+mariadb_engine_version: 10.3.31
+mariadb_engine_version_2: 10.4.21
+
+# For iam roles tests
+postgres_db_instance_class: db.m6g.large # smallest psql instance
+postgres_db_engine_version: 14.2
+
+# For snapshot tests
+snapshot_id: '{{ instance_id }}-ss'
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/files/enhanced_monitoring_assume_policy.json b/tests/integration/targets/rds_instance/roles/rds_instance/files/enhanced_monitoring_assume_policy.json
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/files/enhanced_monitoring_assume_policy.json
@@ -0,0 +1,13 @@
+{
+    "Version": "2012-10-17",
+    "Statement": [
+        {
+            "Sid": "",
+            "Effect": "Allow",
+            "Principal": {
+                "Service": "monitoring.rds.amazonaws.com"
+            },
+            "Action": "sts:AssumeRole"
+        }
+    ]
+}
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/files/s3_integration_policy.json b/tests/integration/targets/rds_instance/roles/rds_instance/files/s3_integration_policy.json
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/files/s3_integration_policy.json
@@ -0,0 +1,16 @@
+{
+  "Version": "2012-10-17",
+  "Statement": [
+      {
+          "Sid": "",
+          "Effect": "Allow",
+          "Action": [
+              "s3:PutObject",
+              "s3:GetObject",
+              "s3:ListBucket",
+              "rds:*"
+          ],
+          "Resource": "*"
+      }
+  ]
+}
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/files/s3_integration_trust_policy.json b/tests/integration/targets/rds_instance/roles/rds_instance/files/s3_integration_trust_policy.json
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/files/s3_integration_trust_policy.json
@@ -0,0 +1,13 @@
+{
+    "Version": "2012-10-17",
+    "Statement": [
+        {
+            "Sid": "",
+            "Effect": "Allow",
+            "Principal": {
+                "Service": "rds.amazonaws.com"
+            },
+            "Action": "sts:AssumeRole"
+        }
+    ]
+}
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/tasks/main.yml b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/main.yml
@@ -0,0 +1,12 @@
+- name: rds_instance integration tests
+  collections:
+  - community.aws
+  module_defaults:
+    group/aws:
+      aws_access_key: '{{ aws_access_key }}'
+      aws_secret_key: '{{ aws_secret_key }}'
+      security_token: '{{ security_token | default(omit) }}'
+      region: '{{ aws_region }}'
+  block:
+  - include: ./test_{{ inventory_hostname }}.yml
+    #- include: ./test_restore_instance.yml  # TODO: snapshot, s3
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_aurora.yml b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_aurora.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_aurora.yml
@@ -0,0 +1,112 @@
+- block:
+
+  - name: Ensure the resource doesn't exist
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+    ignore_errors: yes
+
+  - name: Create minimal aurora cluster in default VPC and default subnet group
+    rds_cluster:
+      state: present
+      engine: aurora
+      cluster_id: '{{ cluster_id }}'
+      username: '{{ username }}'
+      password: '{{ password }}'
+      tags:
+        CreatedBy: rds_instance integration tests
+
+  - name: Create an Aurora instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      cluster_id: '{{ cluster_id }}'
+      engine: aurora
+      state: present
+      db_instance_class: '{{ aurora_db_instance_class }}'
+      tags:
+        CreatedBy: rds_instance integration tests
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.tags | length == 1
+
+  - name: Create an Aurora instance with both username/password and id - invalid
+    rds_instance:
+      id: '{{ instance_id }}-new'
+      cluster_id: '{{ cluster_id }}'
+      engine: aurora
+      state: present
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ aurora_db_instance_class }}'
+      tags:
+        CreatedBy: rds_instance integration tests
+    register: result
+    ignore_errors: yes
+
+  - assert:
+      that:
+      - result.failed
+      - "'Set master user password for the DB Cluster' in result.msg"
+
+  - name: Attempt to modify password (a cluster-managed attribute)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      password: '{{ password }}'
+      force_update_password: true
+      apply_immediately: true
+    register: result
+    ignore_errors: yes
+
+  - assert:
+      that:
+      - result.failed
+      - "'Modify master user password for the DB Cluster using the ModifyDbCluster\
+        \ API' in result.msg"
+      - "'Please see rds_cluster' in result.msg"
+
+  - name: Modify aurora instance port (a cluster-managed attribute)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      port: 1150
+    register: result
+    ignore_errors: yes
+
+  - assert:
+      that:
+      - not result.changed
+      - "'Modify database endpoint port number for the DB Cluster using the ModifyDbCluster\
+        \ API' in result.msg"
+      - "'Please see rds_cluster' in result.msg"
+
+  always:
+
+  - name: Delete the instance
+    rds_instance:
+      id: '{{ item }}'
+      state: absent
+      skip_final_snapshot: true
+      wait: false
+    loop:
+    - '{{ instance_id }}'
+    - '{{ modified_instance_id }}'
+    ignore_errors: yes
+
+  - name: Delete the cluster
+    rds_cluster:
+      cluster_id: '{{ cluster_id }}'
+      state: absent
+      skip_final_snapshot: true
+      wait: false
+    ignore_errors: yes
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_complex.yml b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_complex.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_complex.yml
@@ -0,0 +1,196 @@
+#TODO: test availability_zone and multi_az
+- block:
+  - name: Ensure the resource doesn't exist
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+    ignore_errors: yes
+
+  - name: Create an enhanced monitoring role
+    iam_role:
+      assume_role_policy_document: "{{ lookup('file','files/enhanced_monitoring_assume_policy.json')\
+        \ }}"
+      name: '{{ instance_id }}-role'
+      state: present
+      managed_policy: arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole
+    register: enhanced_monitoring_role
+
+  - name: Create a mariadb instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      engine_version: '{{ mariadb_engine_version }}'
+      allow_major_version_upgrade: true
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ io1_allocated_storage }}'
+      storage_type: '{{ storage_type }}'
+      iops: '{{ iops }}'
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+
+  - name: Add IAM roles to mariab (should fail - iam roles not supported for mariadb)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      engine_version: '{{ mariadb_engine_version }}'
+      allow_major_version_upgrade: true
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ io1_allocated_storage }}'
+      storage_type: '{{ storage_type }}'
+      iops: '{{ iops }}'
+      iam_roles:
+      - role_arn: my_role
+        feature_name: my_feature
+    register: result
+    ignore_errors: true
+
+  - assert:
+      that:
+      - result.failed
+      - '"is not valid for adding IAM roles" in result.msg'
+
+    # TODO: test modifying db_subnet_group_name, db_security_groups, db_parameter_group_name, option_group_name,
+    # monitoring_role_arn, monitoring_interval, domain, domain_iam_role_name, cloudwatch_logs_export_configuration
+
+    # Test multiple modifications including enabling enhanced monitoring
+
+  - name: Modify several attributes - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      allocated_storage: '{{ io1_modified_allocated_storage }}'
+      storage_type: '{{ storage_type }}'
+      db_instance_class: '{{ modified_db_instance_class }}'
+      backup_retention_period: 2
+      preferred_backup_window: 05:00-06:00
+      preferred_maintenance_window: '{{ preferred_maintenance_window }}'
+      auto_minor_version_upgrade: false
+      monitoring_interval: '{{ monitoring_interval }}'
+      monitoring_role_arn: '{{ enhanced_monitoring_role.arn }}'
+      iops: '{{ iops }}'
+      port: 1150
+      max_allocated_storage: 150
+      apply_immediately: true
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Modify several attributes
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      allocated_storage: '{{ io1_modified_allocated_storage }}'
+      storage_type: '{{ storage_type }}'
+      db_instance_class: '{{ modified_db_instance_class }}'
+      backup_retention_period: 2
+      preferred_backup_window: 05:00-06:00
+      preferred_maintenance_window: '{{ preferred_maintenance_window }}'
+      auto_minor_version_upgrade: false
+      monitoring_interval: '{{ monitoring_interval }}'
+      monitoring_role_arn: '{{ enhanced_monitoring_role.arn }}'
+      iops: '{{ iops }}'
+      port: 1150
+      max_allocated_storage: 150
+      apply_immediately: true
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - '"allocated_storage" in result.pending_modified_values or result.allocated_storage
+        == io1_modified_allocated_storage'
+      - '"max_allocated_storage" in result.pending_modified_values or result.max_allocated_storage
+        == 150'
+      - '"port" in result.pending_modified_values or result.endpoint.port == 1150'
+      - '"db_instance_class" in result.pending_modified_values or result.db_instance_class
+        == modified_db_instance_class'
+      - '"monitoring_interval" in result.pending_modified_values or result.monitoring_interval
+        == monitoring_interval'
+
+  - name: Idempotence modifying several pending attributes - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      allocated_storage: '{{ io1_modified_allocated_storage }}'
+      storage_type: '{{ storage_type }}'
+      db_instance_class: '{{ modified_db_instance_class }}'
+      backup_retention_period: 2
+      preferred_backup_window: 05:00-06:00
+      preferred_maintenance_window: '{{ preferred_maintenance_window }}'
+      auto_minor_version_upgrade: false
+      monitoring_interval: '{{ monitoring_interval }}'
+      monitoring_role_arn: '{{ enhanced_monitoring_role.arn }}'
+      iops: '{{ iops }}'
+      port: 1150
+      max_allocated_storage: 150
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Idempotence modifying several pending attributes
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      allocated_storage: '{{ io1_modified_allocated_storage }}'
+      storage_type: '{{ storage_type }}'
+      db_instance_class: '{{ modified_db_instance_class }}'
+      backup_retention_period: 2
+      preferred_backup_window: 05:00-06:00
+      preferred_maintenance_window: '{{ preferred_maintenance_window }}'
+      auto_minor_version_upgrade: false
+      monitoring_interval: '{{ monitoring_interval }}'
+      monitoring_role_arn: '{{ enhanced_monitoring_role.arn }}'
+      iops: '{{ iops }}'
+      port: 1150
+      max_allocated_storage: 150
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - '"allocated_storage" in result.pending_modified_values or result.allocated_storage
+        == io1_modified_allocated_storage'
+      - '"max_allocated_storage" in result.pending_modified_values or result.max_allocated_storage
+        == 150'
+      - '"port" in result.pending_modified_values or result.endpoint.port == 1150'
+      - '"db_instance_class" in result.pending_modified_values or result.db_instance_class
+        == modified_db_instance_class'
+
+  always:
+  - name: Delete the instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+      wait: false
+    ignore_errors: yes
+
+  - name: Remove enhanced monitoring role
+    iam_role:
+      assume_role_policy_document: "{{ lookup('file','files/enhanced_monitoring_assume_policy.json')\
+        \ }}"
+      name: '{{ instance_id }}-role'
+      state: absent
+    ignore_errors: yes
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_iam_roles.yml b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_iam_roles.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_iam_roles.yml
@@ -0,0 +1,336 @@
+- block:
+  - name: Ensure the resource doesn't exist
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+    ignore_errors: yes
+
+  - name: Create postgresql instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: postgres
+      engine_version: '{{ postgres_db_engine_version }}'
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ postgres_db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      allow_major_version_upgrade: yes
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+
+  - name: Create s3 integration policy
+    iam_managed_policy:
+      policy_name: '{{ instance_id }}-s3-policy'
+      policy: "{{ lookup('file','files/s3_integration_policy.json') }}"
+      state: present
+    register: s3_integration_policy
+
+  - name: Create an s3 integration role
+    iam_role:
+      assume_role_policy_document: "{{ lookup('file','files/s3_integration_trust_policy.json')\
+        \ }}"
+      name: '{{ instance_id }}-s3-role-1'
+      state: present
+      managed_policy: '{{ s3_integration_policy.policy.arn }}'
+    register: s3_integration_role_1
+
+  - name: Create an s3 integration role
+    iam_role:
+      assume_role_policy_document: "{{ lookup('file','files/s3_integration_trust_policy.json')\
+        \ }}"
+      name: '{{ instance_id }}-s3-role-2'
+      state: present
+      managed_policy: '{{ s3_integration_policy.policy.arn }}'
+    register: s3_integration_role_2
+
+  - name: Create an s3 integration role
+    iam_role:
+      assume_role_policy_document: "{{ lookup('file','files/s3_integration_trust_policy.json')\
+        \ }}"
+      name: '{{ instance_id }}-s3-role-3'
+      state: present
+      managed_policy: '{{ s3_integration_policy.policy.arn }}'
+    register: s3_integration_role_3
+
+    # ------------------------------------------------------------------------------------------
+
+  - name: Add IAM roles - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      iam_roles:
+      - role_arn: '{{ s3_integration_role_1.arn }}'
+        feature_name: s3Export
+      - role_arn: '{{ s3_integration_role_2.arn }}'
+        feature_name: Lambda
+      - role_arn: '{{ s3_integration_role_3.arn }}'
+        feature_name: s3Import
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Add IAM roles
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      iam_roles:
+      - role_arn: '{{ s3_integration_role_1.arn }}'
+        feature_name: s3Export
+      - role_arn: '{{ s3_integration_role_2.arn }}'
+        feature_name: Lambda
+      - role_arn: '{{ s3_integration_role_3.arn }}'
+        feature_name: s3Import
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.associated_roles | length == 3
+      - "{{ 's3Export' in result.associated_roles | map(attribute='feature_name')\
+        \ }}"
+      - "{{ 'Lambda' in result.associated_roles | map(attribute='feature_name') }}"
+      - "{{ 's3Import' in result.associated_roles | map(attribute='feature_name')\
+        \ }}"
+
+  - name: Add IAM roles (idempotence) - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      iam_roles:
+      - role_arn: '{{ s3_integration_role_1.arn }}'
+        feature_name: s3Export
+      - role_arn: '{{ s3_integration_role_2.arn }}'
+        feature_name: Lambda
+      - role_arn: '{{ s3_integration_role_3.arn }}'
+        feature_name: s3Import
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Add IAM roles (idempotence)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      iam_roles:
+      - role_arn: '{{ s3_integration_role_1.arn }}'
+        feature_name: s3Export
+      - role_arn: '{{ s3_integration_role_2.arn }}'
+        feature_name: Lambda
+      - role_arn: '{{ s3_integration_role_3.arn }}'
+        feature_name: s3Import
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.associated_roles | length == 3
+      - "{{ 's3Export' in result.associated_roles | map(attribute='feature_name')\
+        \ }}"
+      - "{{ 'Lambda' in result.associated_roles | map(attribute='feature_name') }}"
+      - "{{ 's3Import' in result.associated_roles | map(attribute='feature_name')\
+        \ }}"
+
+  - name: Add IAM roles (idempotence) - purge roles
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      iam_roles:
+      - role_arn: '{{ s3_integration_role_1.arn }}'
+        feature_name: s3Export
+      - role_arn: '{{ s3_integration_role_2.arn }}'
+        feature_name: Lambda
+      - role_arn: '{{ s3_integration_role_3.arn }}'
+        feature_name: s3Import
+      purge_iam_roles: yes
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.associated_roles | length == 3
+      - "{{ 's3Export' in result.associated_roles | map(attribute='feature_name')\
+        \ }}"
+      - "{{ 'Lambda' in result.associated_roles | map(attribute='feature_name') }}"
+      - "{{ 's3Import' in result.associated_roles | map(attribute='feature_name')\
+        \ }}"
+
+    # ------------------------------------------------------------------------------------------
+
+  - name: Remove s3Import IAM role from db instance - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      iam_roles:
+      - role_arn: '{{ s3_integration_role_1.arn }}'
+        feature_name: s3Export
+      - role_arn: '{{ s3_integration_role_2.arn }}'
+        feature_name: Lambda
+      purge_iam_roles: yes
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Remove s3Import IAM role from db instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      iam_roles:
+      - role_arn: '{{ s3_integration_role_1.arn }}'
+        feature_name: s3Export
+      - role_arn: '{{ s3_integration_role_2.arn }}'
+        feature_name: Lambda
+      purge_iam_roles: yes
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.associated_roles | length == 2
+      - "{{ 's3Export' in result.associated_roles | map(attribute='feature_name')\
+        \ }}"
+      - "{{ 'Lambda' in result.associated_roles | map(attribute='feature_name') }}"
+      - "{{ 's3Import' not in result.associated_roles | map(attribute='feature_name')\
+        \ }}"
+
+  - name: Remove s3Import IAM role from db instance (idempotence) - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      iam_roles:
+      - role_arn: '{{ s3_integration_role_1.arn }}'
+        feature_name: s3Export
+      - role_arn: '{{ s3_integration_role_2.arn }}'
+        feature_name: Lambda
+      purge_iam_roles: yes
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Remove s3Import IAM role from db instance (idempotence)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      iam_roles:
+      - role_arn: '{{ s3_integration_role_1.arn }}'
+        feature_name: s3Export
+      - role_arn: '{{ s3_integration_role_2.arn }}'
+        feature_name: Lambda
+      purge_iam_roles: yes
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.associated_roles | length == 2
+      - "{{ 's3Export' in result.associated_roles | map(attribute='feature_name')\
+        \ }}"
+      - "{{ 'Lambda' in result.associated_roles | map(attribute='feature_name') }}"
+      - "{{ 's3Import' not in result.associated_roles | map(attribute='feature_name')\
+        \ }}"
+
+    # ------------------------------------------------------------------------------------------
+
+  - name: Remove IAM roles from db instance - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      purge_iam_roles: yes
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Remove IAM roles from db instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      purge_iam_roles: yes
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.associated_roles | length == 0
+
+  - name: Remove IAM roles from db instance (idempotence) - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      purge_iam_roles: yes
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Remove IAM roles from db instance (idempotence)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      purge_iam_roles: yes
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.associated_roles | length == 0
+
+  always:
+  - name: Delete IAM policy
+    iam_managed_policy:
+      policy_name: '{{ instance_id }}-s3-policy'
+      state: absent
+    ignore_errors: yes
+
+  - name: Delete IAM roles
+    iam_role:
+      name: '{{ item.role_name }}'
+      assume_role_policy_document: "{{ lookup('file','files/s3_integration_trust_policy.json')\
+        \ }}"
+      state: absent
+    ignore_errors: yes
+    with_items:
+    - '{{ s3_integration_role_1 }}'
+    - '{{ s3_integration_role_2 }}'
+    - '{{ s3_integration_role_3 }}'
+
+  - name: Delete the instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+      wait: false
+    ignore_errors: yes
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_modify.yml b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_modify.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_modify.yml
@@ -0,0 +1,142 @@
+#TODO: test availability_zone and multi_az
+- block:
+  - name: Ensure the resource doesn't exist
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+    ignore_errors: yes
+
+  - name: Create a mariadb instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      engine_version: '{{ mariadb_engine_version }}'
+      allow_major_version_upgrade: true
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      tags:
+        Name: '{{ instance_id }}'
+        Created_by: Ansible rds_instance tests
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+
+  - name: Create a DB instance with an invalid engine
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: thisisnotavalidengine
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+    register: result
+    ignore_errors: true
+
+  - assert:
+      that:
+      - result.failed
+      - '"value of engine must be one of" in result.msg'
+
+  - name: Add IAM roles to mariadb (should fail - iam roles not supported for mariadb)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      engine_version: '{{ mariadb_engine_version }}'
+      allow_major_version_upgrade: true
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      iam_roles:
+      - role_arn: my_role
+        feature_name: my_feature
+    register: result
+    ignore_errors: true
+
+  - assert:
+      that:
+      - result.failed
+      - '"is not valid for adding IAM roles" in result.msg'
+
+    # TODO: test modifying db_subnet_group_name, db_security_groups, db_parameter_group_name, option_group_name,
+    # monitoring_role_arn, monitoring_interval, domain, domain_iam_role_name, cloudwatch_logs_export_configuration
+
+    # ------------------------------------------------------------------------------------------
+
+  - name: Modify the instance name without immediate application - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      new_id: '{{ modified_instance_id }}'
+      apply_immediately: false
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Modify the instance name without immediate application
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      new_id: '{{ modified_instance_id }}'
+      apply_immediately: false
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == "{{ instance_id }}"
+
+  - name: Immediately apply the pending update - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      new_id: '{{ modified_instance_id }}'
+      apply_immediately: true
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Immediately apply the pending update
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      new_id: '{{ modified_instance_id }}'
+      apply_immediately: true
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == "{{ modified_instance_id }}"
+
+  always:
+  - name: Delete the instance
+    rds_instance:
+      id: '{{ item }}'
+      state: absent
+      skip_final_snapshot: true
+      wait: false
+    ignore_errors: yes
+    loop:
+    - '{{ instance_id }}'
+    - '{{ modified_instance_id }}'
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_processor.yml b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_processor.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_processor.yml
@@ -0,0 +1,132 @@
+- block:
+
+  - name: Ensure the resource doesn't exist
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+    ignore_errors: yes
+
+  - name: Create an oracle-ee DB instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: oracle-ee
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ oracle_ee_db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      storage_encrypted: true
+      processor_features: {}
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Modify the processor features - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: oracle-ee
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ oracle_ee_db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      storage_encrypted: true
+      processor_features: '{{ modified_processor_features }}'
+      apply_immediately: true
+    register: result
+    check_mode: true
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Modify the processor features
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: oracle-ee
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ oracle_ee_db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      storage_encrypted: true
+      processor_features: '{{ modified_processor_features }}'
+      apply_immediately: true
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.pending_modified_values.processor_features.coreCount == "{{ modified_processor_features.coreCount
+        }}"
+      - result.pending_modified_values.processor_features.threadsPerCore == "{{ modified_processor_features.threadsPerCore
+        }}"
+
+  - name: Modify the processor features (idempotence) - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: oracle-ee
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ oracle_ee_db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      storage_encrypted: true
+      processor_features: '{{ modified_processor_features }}'
+      apply_immediately: true
+    register: result
+    check_mode: true
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Modify the processor features (idempotence)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: oracle-ee
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ oracle_ee_db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      storage_encrypted: true
+      processor_features: '{{ modified_processor_features }}'
+      apply_immediately: true
+    register: result
+
+      # Check if processor features either are pending or already changed
+  - assert:
+      that:
+      - not result.changed
+      - (result.pending_modified_values.processor_features.coreCount is defined and
+        result.pending_modified_values.processor_features.coreCount == "{{ modified_processor_features.coreCount
+        }}") or (result.processor_features.coreCount is defined and result.processor_features.coreCount
+        == "{{ modified_processor_features.coreCount }}")
+      - (result.pending_modified_values.processor_features.threadsPerCore is defined
+        and result.pending_modified_values.processor_features.threadsPerCore == "{{
+        modified_processor_features.threadsPerCore }}") or (result.processor_features.threadsPerCore
+        is defined and result.processor_features.threadsPerCore == "{{ modified_processor_features.threadsPerCore
+        }}")
+
+  always:
+
+  - name: Delete the DB instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+      wait: false
+    register: result
+
+  - assert:
+      that:
+      - result.changed
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_replica.yml b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_replica.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_replica.yml
@@ -0,0 +1,225 @@
+- block:
+
+  - name: set the two regions for the source DB and the replica
+    set_fact:
+      region_src: '{{ aws_region }}'
+      region_dest: '{{ aws_region }}'
+
+  - name: Ensure the resource doesn't exist
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+      region: '{{ region_src }}'
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+    ignore_errors: yes
+
+  - name: Create a source DB instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mysql
+      backup_retention_period: 1
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      region: '{{ region_src }}'
+      tags:
+        Name: '{{ instance_id }}'
+        Created_by: Ansible rds_instance tests
+    register: source_db
+
+  - assert:
+      that:
+      - source_db.changed
+      - source_db.db_instance_identifier == '{{ instance_id }}'
+
+    # ------------------------------------------------------------------------------------------
+
+  - name: Create a read replica in a different region - check_mode
+    rds_instance:
+      id: '{{ instance_id }}-replica'
+      state: present
+      source_db_instance_identifier: '{{ instance_id }}'
+      engine: mysql
+      username: '{{ username }}'
+      password: '{{ password }}'
+      read_replica: true
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      region: '{{ region_dest }}'
+      tags:
+        Name: '{{ instance_id }}'
+        Created_by: Ansible rds_instance tests
+      wait: yes
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Create a read replica in a different region
+    rds_instance:
+      id: '{{ instance_id }}-replica'
+      state: present
+      source_db_instance_identifier: '{{ instance_id }}'
+      engine: mysql
+      username: '{{ username }}'
+      password: '{{ password }}'
+      read_replica: true
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      region: '{{ region_dest }}'
+      tags:
+        Name: '{{ instance_id }}'
+        Created_by: Ansible rds_instance tests
+      wait: yes
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}-replica'
+      - result.tags | length == 2
+      - result.tags.Name == '{{ instance_id }}'
+      - result.tags.Created_by == 'Ansible rds_instance tests'
+
+  - name: Test idempotence with a read replica - check_mode
+    rds_instance:
+      id: '{{ instance_id }}-replica'
+      state: present
+      source_db_instance_identifier: '{{ instance_id }}'
+      engine: mysql
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      region: '{{ region_dest }}'
+      tags:
+        Name: '{{ instance_id }}'
+        Created_by: Ansible rds_instance tests
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Test idempotence with a read replica
+    rds_instance:
+      id: '{{ instance_id }}-replica'
+      state: present
+      source_db_instance_identifier: '{{ instance_id }}'
+      engine: mysql
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      region: '{{ region_dest }}'
+      tags:
+        Name: '{{ instance_id }}'
+        Created_by: Ansible rds_instance tests
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Test idempotence with read_replica=True
+    rds_instance:
+      id: '{{ instance_id }}-replica'
+      state: present
+      read_replica: true
+      source_db_instance_identifier: '{{ instance_id }}'
+      engine: mysql
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      region: '{{ region_dest }}'
+      tags:
+        Name: '{{ instance_id }}'
+        Created_by: Ansible rds_instance tests
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+
+    # ------------------------------------------------------------------------------------------
+
+  - name: Promote the read replica - check_mode
+    rds_instance:
+      id: '{{ instance_id }}-replica'
+      state: present
+      read_replica: false
+      region: '{{ region_dest }}'
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Promote the read replica
+    rds_instance:
+      id: '{{ instance_id }}-replica'
+      state: present
+      read_replica: false
+      region: '{{ region_dest }}'
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Test idempotence - check_mode
+    rds_instance:
+      id: '{{ instance_id }}-replica'
+      state: present
+      read_replica: false
+      region: '{{ region_dest }}'
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Test idempotence
+    rds_instance:
+      id: '{{ instance_id }}-replica'
+      state: present
+      read_replica: false
+      region: '{{ region_dest }}'
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+
+  always:
+
+  - name: Remove the DB instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+      region: '{{ region_src }}'
+      wait: false
+    ignore_errors: yes
+
+  - name: Remove the DB replica
+    rds_instance:
+      id: '{{ instance_id }}-replica'
+      state: absent
+      skip_final_snapshot: true
+      region: '{{ region_dest }}'
+      wait: false
+    ignore_errors: yes
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_restore.yml b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_restore.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_restore.yml
@@ -0,0 +1,122 @@
+- block:
+     # TODO: snapshot, s3
+
+  - name: Ensure the resource doesn't exist
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+    ignore_errors: yes
+
+  - name: Create a source DB instance
+    rds_instance:
+      id: '{{ instance_id }}-s'
+      state: present
+      engine: mysql
+      backup_retention_period: 1
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+    register: source_db
+
+  - assert:
+      that:
+      - source_db.changed
+      - source_db.db_instance_identifier == '{{ instance_id }}-s'
+
+  - name: Create a point in time DB instance - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      source_db_instance_identifier: '{{ instance_id }}-s'
+      creation_source: instance
+      engine: mysql
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      use_latest_restorable_time: true
+    register: result
+    check_mode: yes
+
+  - assert:
+      that: result.changed
+
+  - name: Create a point in time DB instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      source_db_instance_identifier: '{{ instance_id }}-s'
+      creation_source: instance
+      engine: mysql
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      use_latest_restorable_time: true
+    register: result
+
+  - assert:
+      that: result.changed
+
+  - name: Create a point in time DB instance (idempotence) - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      source_db_instance_identifier: '{{ instance_id }}-s'
+      creation_source: instance
+      engine: mysql
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      restore_time: '{{ result.latest_restorable_time }}'
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Create a point in time DB instance (idempotence)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      source_db_instance_identifier: '{{ instance_id }}-s'
+      creation_source: instance
+      engine: mysql
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      restore_time: '{{ result.latest_restorable_time }}'
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+
+  always:
+
+  - name: Remove the DB instance
+    rds_instance:
+      id: '{{ instance_id }}-s'
+      state: absent
+      skip_final_snapshot: true
+      wait: false
+    ignore_errors: yes
+
+  - name: Remove the point in time restored DB
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+      wait: false
+    ignore_errors: yes
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_sgroups.yml b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_sgroups.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_sgroups.yml
@@ -0,0 +1,324 @@
+- name: rds_instance test_vpc_security_groups tests
+  block:
+
+  - name: create a VPC
+    ec2_vpc_net:
+      name: '{{ resource_prefix }}-vpc'
+      state: present
+      cidr_block: 10.122.122.128/26
+      tags:
+        Name: '{{ resource_prefix }}-vpc'
+        Description: created by rds_instance integration tests
+    register: vpc_result
+
+  - name: create subnets
+    ec2_vpc_subnet:
+      cidr: '{{ item.cidr }}'
+      az: '{{ item.zone }}'
+      vpc_id: '{{ vpc_result.vpc.id }}'
+      tags:
+        Name: '{{ resource_prefix }}-subnet'
+        Description: created by rds_instance integration tests
+      state: present
+    register: subnets_result
+    loop:
+    - {cidr: 10.122.122.128/28, zone: '{{ aws_region }}a'}
+    - {cidr: 10.122.122.144/28, zone: '{{ aws_region }}b'}
+    - {cidr: 10.122.122.160/28, zone: '{{ aws_region }}c'}
+
+  - name: Create security groups
+    ec2_group:
+      name: '{{ item }}'
+      description: created by rds_instance integration tests
+      state: present
+    register: sgs_result
+    loop:
+    - '{{ resource_prefix }}-sg-1'
+    - '{{ resource_prefix }}-sg-2'
+    - '{{ resource_prefix }}-sg-3'
+
+  - name: Ensure the resource doesn't exist
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+    ignore_errors: yes
+
+    # ------------------------------------------------------------------------------------------
+
+  - name: Create a DB instance in the VPC with two security groups - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      vpc_security_group_ids:
+      - '{{ sgs_result.results.0.group_id }}'
+      - '{{ sgs_result.results.1.group_id }}'
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Create a DB instance in the VPC with two security groups
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      vpc_security_group_ids:
+      - '{{ sgs_result.results.0.group_id }}'
+      - '{{ sgs_result.results.1.group_id }}'
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.vpc_security_groups | selectattr('status', 'in', ['active', 'adding'])
+        | list | length == 2
+
+  - name: Create a DB instance in the VPC with two security groups (idempotence) -
+      check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      vpc_security_group_ids:
+      - '{{ sgs_result.results.0.group_id }}'
+      - '{{ sgs_result.results.1.group_id }}'
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Create a DB instance in the VPC with two security groups (idempotence)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      vpc_security_group_ids:
+      - '{{ sgs_result.results.0.group_id }}'
+      - '{{ sgs_result.results.1.group_id }}'
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.vpc_security_groups | selectattr('status', 'in', ['active', 'adding'])
+        | list | length == 2
+
+    # ------------------------------------------------------------------------------------------
+
+  - name: Add a new security group without purge - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      vpc_security_group_ids:
+      - '{{ sgs_result.results.2.group_id }}'
+      apply_immediately: true
+      purge_security_groups: false
+    check_mode: true
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+
+  - name: Add a new security group without purge
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      vpc_security_group_ids:
+      - '{{ sgs_result.results.2.group_id }}'
+      apply_immediately: true
+      purge_security_groups: false
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.vpc_security_groups | selectattr('status', 'in', ['active', 'adding'])
+        | list | length == 3
+
+  - name: Add a new security group without purge (idempotence) - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      vpc_security_group_ids:
+      - '{{ sgs_result.results.2.group_id }}'
+      apply_immediately: true
+      purge_security_groups: false
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+
+  - name: Add a new security group without purge (idempotence)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      vpc_security_group_ids:
+      - '{{ sgs_result.results.2.group_id }}'
+      apply_immediately: true
+      purge_security_groups: false
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.vpc_security_groups | selectattr('status', 'in', ['active', 'adding'])
+        | list | length == 3
+
+    # ------------------------------------------------------------------------------------------
+
+  - name: Add a security group with purge - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      vpc_security_group_ids:
+      - '{{ sgs_result.results.2.group_id }}'
+      apply_immediately: true
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Add a security group with purge
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      vpc_security_group_ids:
+      - '{{ sgs_result.results.2.group_id }}'
+      apply_immediately: true
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.vpc_security_groups | selectattr('status', 'in', ['active', 'adding'])
+        | list | length == 1
+      - result.vpc_security_groups | selectattr('status', 'equalto', 'removing') |
+        list | length == 2
+
+  - name: Add a security group with purge (idempotence) - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      vpc_security_group_ids:
+      - '{{ sgs_result.results.2.group_id }}'
+      apply_immediately: true
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Add a security group with purge (idempotence)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      vpc_security_group_ids:
+      - '{{ sgs_result.results.2.group_id }}'
+      apply_immediately: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.vpc_security_groups | selectattr('status', 'in', ['active', 'adding'])
+        | list | length == 1
+
+  always:
+
+  - name: Ensure the resource doesn't exist
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+    ignore_errors: yes
+
+  - name: Remove security groups
+    ec2_group:
+      name: '{{ item }}'
+      description: created by rds_instance integration tests
+      state: absent
+    register: sgs_result
+    loop:
+    - '{{ resource_prefix }}-sg-1'
+    - '{{ resource_prefix }}-sg-2'
+    - '{{ resource_prefix }}-sg-3'
+    ignore_errors: yes
+    retries: 30
+    until: sgs_result is not failed
+    delay: 10
+
+  - name: remove subnets
+    ec2_vpc_subnet:
+      cidr: '{{ item.cidr }}'
+      az: '{{ item.zone }}'
+      vpc_id: '{{ vpc_result.vpc.id }}'
+      tags:
+        Name: '{{ resource_prefix }}-subnet'
+        Description: created by rds_instance integration tests
+      state: absent
+    register: subnets
+    ignore_errors: yes
+    retries: 30
+    until: subnets is not failed
+    delay: 10
+    loop:
+    - {cidr: 10.122.122.128/28, zone: '{{ aws_region }}a'}
+    - {cidr: 10.122.122.144/28, zone: '{{ aws_region }}b'}
+    - {cidr: 10.122.122.160/28, zone: '{{ aws_region }}c'}
+    - {cidr: 10.122.122.176/28, zone: '{{ aws_region }}d'}
+
+  - name: Delete VPC
+    ec2_vpc_net:
+      name: '{{ resource_prefix }}-vpc'
+      state: absent
+      cidr_block: 10.122.122.128/26
+      tags:
+        Name: '{{ resource_prefix }}-vpc'
+        Description: created by rds_instance integration tests
+    register: vpc_result
+    ignore_errors: yes
+    retries: 30
+    until: vpc_result is not failed
+    delay: 10
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_snapshot.yml b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_snapshot.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_snapshot.yml
@@ -0,0 +1,215 @@
+- block:
+  - name: Ensure the resource doesn't exist
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+    ignore_errors: yes
+
+  - name: Create a mariadb instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      tags:
+        Name: '{{ instance_id }}'
+        Created_by: Ansible rds_instance tests
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.tags | length == 2
+      - result.tags.Name == '{{ instance_id }}'
+      - result.tags.Created_by == 'Ansible rds_instance tests'
+
+  - name: Create a snapshot
+    rds_instance_snapshot:
+      instance_id: '{{ instance_id }}'
+      snapshot_id: '{{ snapshot_id }}'
+      state: present
+      wait: yes
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == "{{ instance_id }}"
+      - result.db_snapshot_identifier == "{{ snapshot_id }}"
+
+    # ------------------------------------------------------------------------------------------
+    # Test restoring db from snapshot
+
+  - name: Restore DB from snapshot - check_mode
+    rds_instance:
+      id: '{{ snapshot_id }}'
+      creation_source: snapshot
+      snapshot_identifier: '{{ snapshot_id }}'
+      engine: mariadb
+      state: present
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Restore DB from snapshot
+    rds_instance:
+      id: '{{ snapshot_id }}'
+      creation_source: snapshot
+      snapshot_identifier: '{{ snapshot_id }}'
+      engine: mariadb
+      state: present
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == "{{ snapshot_id }}"
+      - result.tags | length == 2
+      - result.tags.Name == "{{ instance_id }}"
+      - result.tags.Created_by == 'Ansible rds_instance tests'
+      - result.db_instance_status == 'available'
+
+  - name: Restore DB from snapshot (idempotence) - check_mode
+    rds_instance:
+      id: '{{ snapshot_id }}'
+      creation_source: snapshot
+      snapshot_identifier: '{{ snapshot_id }}'
+      engine: mariadb
+      state: present
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Restore DB from snapshot (idempotence)
+    rds_instance:
+      id: '{{ snapshot_id }}'
+      creation_source: snapshot
+      snapshot_identifier: '{{ snapshot_id }}'
+      engine: mariadb
+      state: present
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - result.db_instance_identifier == "{{ snapshot_id }}"
+      - result.tags | length == 2
+      - result.tags.Name == "{{ instance_id }}"
+      - result.tags.Created_by == 'Ansible rds_instance tests'
+      - result.db_instance_status == 'available'
+
+    # ------------------------------------------------------------------------------------------
+    # Test final snapshot on deletion
+
+  - name: Ensure instance exists prior to deleting
+    rds_instance_info:
+      db_instance_identifier: '{{ instance_id }}'
+    register: db_info
+
+  - assert:
+      that:
+      - db_info.instances | length == 1
+
+  - name: Delete the instance keeping snapshot - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      final_snapshot_identifier: '{{ instance_id }}'
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Delete the instance keeping snapshot
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      final_snapshot_identifier: '{{ instance_id }}'
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.final_snapshot.db_instance_identifier == '{{ instance_id }}'
+
+  - name: Check that snapshot exists
+    rds_snapshot_info:
+      db_snapshot_identifier: '{{ instance_id }}'
+    register: result
+
+  - assert:
+      that:
+      - result.snapshots | length == 1
+      - result.snapshots.0.engine == 'mariadb'
+
+  - name: Ensure instance was deleted
+    rds_instance_info:
+      db_instance_identifier: '{{ instance_id }}'
+    register: db_info
+
+  - assert:
+      that:
+      - db_info.instances | length == 0
+
+  - name: Delete the instance (idempotence) - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Delete the instance (idempotence)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+
+  always:
+  - name: Remove snapshots
+    rds_instance_snapshot:
+      db_snapshot_identifier: '{{ item }}'
+      state: absent
+      wait: false
+    ignore_errors: yes
+    with_items:
+    - '{{ instance_id }}'
+    - '{{ snapshot_id }}'
+
+  - name: Remove DB instances
+    rds_instance:
+      id: '{{ item }}'
+      state: absent
+      skip_final_snapshot: true
+      wait: false
+    ignore_errors: yes
+    with_items:
+    - '{{ instance_id }}'
+    - '{{ snapshot_id }}'
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_states.yml b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_states.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_states.yml
@@ -0,0 +1,311 @@
+- block:
+  - name: Ensure the resource doesn't exist
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+    ignore_errors: yes
+
+  - name: Create a mariadb instance - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      deletion_protection: true
+      tags:
+        Name: '{{ instance_id }}'
+        Created_by: Ansible rds_instance tests
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Create a mariadb instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      deletion_protection: true
+      tags:
+        Name: '{{ instance_id }}'
+        Created_by: Ansible rds_instance tests
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.tags | length == 2
+      - result.tags.Name == '{{ instance_id }}'
+      - result.tags.Created_by == 'Ansible rds_instance tests'
+      - result.deletion_protection == True
+
+  - name: Create a mariadb instance (idempotence) - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      deletion_protection: true
+      tags:
+        Name: '{{ instance_id }}'
+        Created_by: Ansible rds_instance tests
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Create a mariadb instance (idempotence)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      deletion_protection: true
+      tags:
+        Name: '{{ instance_id }}'
+        Created_by: Ansible rds_instance tests
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.tags | length == 2
+      - result.tags.Name == '{{ instance_id }}'
+      - result.tags.Created_by == 'Ansible rds_instance tests'
+      - result.deletion_protection == True
+
+    # ------------------------------------------------------------------------------------------
+    # Test stopping / rebooting instances
+
+  - name: Reboot a stopped instance - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: rebooted
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Reboot a stopped instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: rebooted
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+
+    # ------------------------------------------------------------------------------------------
+
+  - name: Stop the instance - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: stopped
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Stop the instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: stopped
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Stop the instance (idempotence) - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: stopped
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Stop the instance (idempotence)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: stopped
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+
+    # ------------------------------------------------------------------------------------------
+
+  - name: Start the instance - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: started
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Start the instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: started
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Start the instance (idempotence) - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: started
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Start the instance (idempotence)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: started
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+
+    # ------------------------------------------------------------------------------------------
+    # Test deletion protection / deletion
+
+  - name: Ensure instance exists prior to deleting
+    rds_instance_info:
+      db_instance_identifier: '{{ instance_id }}'
+    register: db_info
+
+  - assert:
+      that:
+      - db_info.instances | length == 1
+
+  - name: Attempt to delete DB instance with deletion protection (should fail)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+    ignore_errors: yes
+
+  - assert:
+      that:
+      - result.failed
+
+  - name: Turn off deletion protection
+    rds_instance:
+      id: '{{ instance_id }}'
+      deletion_protection: false
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.deletion_protection == False
+
+  - name: Delete the instance - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Delete the instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Ensure instance was deleted
+    rds_instance_info:
+      db_instance_identifier: '{{ instance_id }}'
+    register: db_info
+
+  - assert:
+      that:
+      - db_info.instances | length == 0
+
+  - name: Delete the instance (idempotence) - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Delete the instance (idempotence)
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+
+  always:
+  - name: Remove DB instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+      wait: false
+    ignore_errors: yes
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_tagging.yml b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_tagging.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_tagging.yml
@@ -0,0 +1,190 @@
+- block:
+  - name: Ensure the resource doesn't exist
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+    ignore_errors: yes
+
+    # Test invalid bad options
+  - name: Create a DB instance with an invalid engine
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: thisisnotavalidengine
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+    register: result
+    ignore_errors: true
+
+  - assert:
+      that:
+      - result.failed
+      - '"value of engine must be one of" in result.msg'
+
+    # Test creation, adding tags and enabling encryption
+  - name: Create a mariadb instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      storage_encrypted: true
+      tags:
+        Name: '{{ instance_id }}'
+        Created_by: Ansible rds_instance tests
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+      - result.tags | length == 2
+      - result.tags.Name == '{{ instance_id }}'
+      - result.tags.Created_by == 'Ansible rds_instance tests'
+      - result.kms_key_id
+      - result.storage_encrypted == true
+
+  - name: Test impotency omitting tags - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Test impotency omitting tags
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - result.db_instance_identifier
+      - result.tags | length == 2
+
+  - name: Idempotence with minimal options
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - result.db_instance_identifier
+      - result.tags | length == 2
+
+  - name: Test tags are not purged if purge_tags is False
+    rds_instance:
+      db_instance_identifier: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      tags: {}
+      purge_tags: false
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - result.tags | length == 2
+
+  - name: Add a tag and remove a tag - check_mode
+    rds_instance:
+      db_instance_identifier: '{{ instance_id }}'
+      state: present
+      tags:
+        Name: '{{ instance_id }}-new'
+        Created_by: Ansible rds_instance tests
+      purge_tags: true
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Add a tag and remove a tag
+    rds_instance:
+      db_instance_identifier: '{{ instance_id }}'
+      state: present
+      tags:
+        Name: '{{ instance_id }}-new'
+        Created_by: Ansible rds_instance tests
+      purge_tags: true
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.tags | length == 2
+      - result.tags.Name == '{{ instance_id }}-new'
+
+  - name: Add a tag and remove a tag (idempotence) - check_mode
+    rds_instance:
+      db_instance_identifier: '{{ instance_id }}'
+      state: present
+      tags:
+        Name: '{{ instance_id }}-new'
+        Created_by: Ansible rds_instance tests
+      purge_tags: true
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - not result.changed
+
+  - name: Add a tag and remove a tag (idempotence)
+    rds_instance:
+      db_instance_identifier: '{{ instance_id }}'
+      state: present
+      tags:
+        Name: '{{ instance_id }}-new'
+        Created_by: Ansible rds_instance tests
+      purge_tags: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - result.tags | length == 2
+      - result.tags.Name == '{{ instance_id }}-new'
+
+  always:
+  - name: Remove DB instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+      wait: false
+    ignore_errors: yes
diff --git a/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_upgrade.yml b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_upgrade.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance/roles/rds_instance/tasks/test_upgrade.yml
@@ -0,0 +1,119 @@
+- block:
+  - name: Ensure the resource doesn't exist
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+    ignore_errors: yes
+
+  - name: Create a mariadb instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      engine_version: '{{ mariadb_engine_version }}'
+      allow_major_version_upgrade: true
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - result.db_instance_identifier == '{{ instance_id }}'
+
+    # Test upgrade of DB instance
+
+  - name: Upgrade a mariadb instance - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      engine_version: '{{ mariadb_engine_version_2 }}'
+      allow_major_version_upgrade: true
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      apply_immediately: true
+    register: result
+    check_mode: yes
+
+  - assert:
+      that:
+      - result.changed
+
+  - name: Upgrade a mariadb instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      engine_version: '{{ mariadb_engine_version_2 }}'
+      allow_major_version_upgrade: true
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+      apply_immediately: true
+    register: result
+
+  - assert:
+      that:
+      - result.changed
+      - '"engine_version" in result.pending_modified_values or result.engine_version
+        == mariadb_engine_version_2'
+
+  - name: Idempotence upgrading a mariadb instance - check_mode
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      engine_version: '{{ mariadb_engine_version_2 }}'
+      allow_major_version_upgrade: true
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+    register: result
+    check_mode: yes
+
+    ### Specifying allow_major_version_upgrade with check_mode will always result in changed=True
+    ### since it's not returned in describe_db_instances api call
+    # - assert:
+    #     that:
+    #       - not result.changed
+
+  - name: Idempotence upgrading a mariadb instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: present
+      engine: mariadb
+      engine_version: '{{ mariadb_engine_version_2 }}'
+      allow_major_version_upgrade: true
+      username: '{{ username }}'
+      password: '{{ password }}'
+      db_instance_class: '{{ db_instance_class }}'
+      allocated_storage: '{{ allocated_storage }}'
+    register: result
+
+  - assert:
+      that:
+      - not result.changed
+      - '"engine_version" in result.pending_modified_values or result.engine_version
+        == mariadb_engine_version_2'
+
+  always:
+  - name: Delete the instance
+    rds_instance:
+      id: '{{ instance_id }}'
+      state: absent
+      skip_final_snapshot: true
+      wait: false
+    ignore_errors: yes
diff --git a/tests/integration/targets/rds_instance/runme.sh b/tests/integration/targets/rds_instance/runme.sh
new file mode 100755
--- /dev/null
+++ b/tests/integration/targets/rds_instance/runme.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+#
+# Beware: most of our tests here are run in parallel.
+# To add new tests you'll need to add a new host to the inventory and a matching
+# '{{ inventory_hostname }}'.yml file in roles/ec2_instance/tasks/
+
+
+set -eux
+
+export ANSIBLE_ROLES_PATH=../
+
+ansible-playbook main.yml -i inventory "$@"
diff --git a/tests/integration/targets/rds_instance_snapshot/aliases b/tests/integration/targets/rds_instance_snapshot/aliases
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance_snapshot/aliases
@@ -0,0 +1,5 @@
+slow
+
+cloud/aws
+
+rds_snapshot_info
diff --git a/tests/integration/targets/rds_instance_snapshot/defaults/main.yml b/tests/integration/targets/rds_instance_snapshot/defaults/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance_snapshot/defaults/main.yml
@@ -0,0 +1,14 @@
+---
+# defaults file for rds_instance_snapshot
+
+# Create RDS instance
+instance_id: '{{ resource_prefix }}-instance'
+username: 'testrdsusername'
+password: "{{ lookup('password', '/dev/null') }}"
+db_instance_class: db.t3.micro
+allocated_storage: 10
+engine: 'mariadb'
+mariadb_engine_version: 10.3.31
+
+# Create snapshot
+snapshot_id: '{{ instance_id }}-snapshot'
diff --git a/tests/integration/targets/rds_instance_snapshot/tasks/main.yml b/tests/integration/targets/rds_instance_snapshot/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance_snapshot/tasks/main.yml
@@ -0,0 +1,505 @@
+---
+- module_defaults:
+    group/aws:
+      region: "{{ aws_region }}"
+      aws_access_key: "{{ aws_access_key }}"
+      aws_secret_key: "{{ aws_secret_key }}"
+      security_token: "{{ security_token | default(omit) }}"
+  collections:
+    - community.aws
+    - amazon.aws
+
+  block:
+  - name: Create a source mariadb instance
+    rds_instance:
+      id: "{{ instance_id }}"
+      state: present
+      engine: "{{ engine}}"
+      engine_version: "{{ mariadb_engine_version }}"
+      allow_major_version_upgrade: true
+      username: "{{ username }}"
+      password: "{{ password }}"
+      db_instance_class: "{{ db_instance_class }}"
+      allocated_storage: "{{ allocated_storage }}"
+    register: _result_create_instance
+
+  - assert:
+      that:
+      - _result_create_instance.changed
+      - _result_create_instance.db_instance_identifier == "{{ instance_id }}"
+
+  - name: Get all RDS snapshots for the existing instance
+    rds_snapshot_info:
+      db_instance_identifier: "{{ instance_id }}"
+    register: _result_instance_snapshot_info
+
+  - assert:
+      that:
+      - _result_instance_snapshot_info is successful
+      - _result_instance_snapshot_info.snapshots | length == 1
+
+  - name: Take a snapshot of the existing RDS instance (CHECK_MODE)
+    rds_instance_snapshot:
+      state: present
+      db_instance_identifier: "{{ instance_id }}"
+      db_snapshot_identifier: "{{ snapshot_id }}"
+    check_mode: yes
+    register: _result_instance_snapshot
+
+  - assert:
+      that:
+      - _result_instance_snapshot.changed
+
+  - name: Take a snapshot of the existing RDS instance
+    rds_instance_snapshot:
+      state: present
+      db_instance_identifier: "{{ instance_id }}"
+      db_snapshot_identifier: "{{ snapshot_id }}"
+      wait: true
+    register: _result_instance_snapshot
+
+  - assert:
+      that:
+      - _result_instance_snapshot.changed
+      - "'availability_zone' in _result_instance_snapshot"
+      - "'instance_create_time' in _result_instance_snapshot"
+      - "'db_instance_identifier' in _result_instance_snapshot"
+      - _result_instance_snapshot.db_instance_identifier == "{{ instance_id }}"
+      - "'db_snapshot_identifier' in _result_instance_snapshot"
+      - _result_instance_snapshot.db_snapshot_identifier == "{{ snapshot_id }}"
+      - "'db_snapshot_arn' in _result_instance_snapshot"
+      - "'dbi_resource_id' in _result_instance_snapshot"
+      - "'encrypted' in _result_instance_snapshot"
+      - "'engine' in _result_instance_snapshot"
+      - _result_instance_snapshot.engine == "{{ engine }}"
+      - "'engine_version' in _result_instance_snapshot"
+      - _result_instance_snapshot.engine_version == "{{ mariadb_engine_version }}"
+      - "'iam_database_authentication_enabled' in _result_instance_snapshot"
+      - "'license_model' in _result_instance_snapshot"
+      - "'master_username' in _result_instance_snapshot"
+      - _result_instance_snapshot.master_username == "{{ username }}"
+      - "'snapshot_create_time' in _result_instance_snapshot"
+      - "'snapshot_type' in _result_instance_snapshot"
+      - "'status' in _result_instance_snapshot"
+      - _result_instance_snapshot.status == "available"
+      - "'snapshot_type' in _result_instance_snapshot"
+      - _result_instance_snapshot.snapshot_type == "manual"
+      - "'status' in _result_instance_snapshot"
+      - "'storage_type' in _result_instance_snapshot"
+      - _result_instance_snapshot.storage_type == "gp2"
+      - "'tags' in _result_instance_snapshot"
+      - "'vpc_id' in _result_instance_snapshot"
+
+  - name: Take a snapshot of the existing RDS instance (CHECK_MODE - IDEMPOTENCE)
+    rds_instance_snapshot:
+      state: present
+      db_instance_identifier: "{{ instance_id }}"
+      db_snapshot_identifier: "{{ snapshot_id }}"
+    check_mode: yes
+    register: _result_instance_snapshot
+
+  - assert:
+      that:
+      - not _result_instance_snapshot.changed
+
+  - name: Take a snapshot of the existing RDS instance (IDEMPOTENCE)
+    rds_instance_snapshot:
+      state: present
+      db_instance_identifier: "{{ instance_id }}"
+      db_snapshot_identifier: "{{ snapshot_id }}"
+      wait: true
+    register: _result_instance_snapshot
+
+  - assert:
+      that:
+      - not _result_instance_snapshot.changed
+      - "'availability_zone' in _result_instance_snapshot"
+      - "'instance_create_time' in _result_instance_snapshot"
+      - "'db_instance_identifier' in _result_instance_snapshot"
+      - _result_instance_snapshot.db_instance_identifier == "{{ instance_id }}"
+      - "'db_snapshot_identifier' in _result_instance_snapshot"
+      - _result_instance_snapshot.db_snapshot_identifier == "{{ snapshot_id }}"
+      - "'db_snapshot_arn' in _result_instance_snapshot"
+      - "'dbi_resource_id' in _result_instance_snapshot"
+      - "'encrypted' in _result_instance_snapshot"
+      - "'engine' in _result_instance_snapshot"
+      - _result_instance_snapshot.engine == "{{ engine }}"
+      - "'engine_version' in _result_instance_snapshot"
+      - _result_instance_snapshot.engine_version == "{{ mariadb_engine_version }}"
+      - "'iam_database_authentication_enabled' in _result_instance_snapshot"
+      - "'license_model' in _result_instance_snapshot"
+      - "'master_username' in _result_instance_snapshot"
+      - _result_instance_snapshot.master_username == "{{ username }}"
+      - "'snapshot_create_time' in _result_instance_snapshot"
+      - "'snapshot_type' in _result_instance_snapshot"
+      - "'status' in _result_instance_snapshot"
+      - _result_instance_snapshot.status == "available"
+      - "'snapshot_type' in _result_instance_snapshot"
+      - _result_instance_snapshot.snapshot_type == "manual"
+      - "'status' in _result_instance_snapshot"
+      - "'storage_type' in _result_instance_snapshot"
+      - _result_instance_snapshot.storage_type == "gp2"
+      - "'tags' in _result_instance_snapshot"
+      - "'vpc_id' in _result_instance_snapshot"
+
+  - name: Get information about the existing DB snapshot
+    rds_snapshot_info:
+      db_snapshot_identifier: "{{ snapshot_id }}"
+    register: _result_instance_snapshot_info
+
+  - assert:
+      that:
+      - _result_instance_snapshot_info is successful
+      - _result_instance_snapshot_info.snapshots[0].db_instance_identifier == "{{ instance_id }}"
+      - _result_instance_snapshot_info.snapshots[0].db_snapshot_identifier == "{{ snapshot_id }}"
+
+  - name: Take another snapshot of the existing RDS instance
+    rds_instance_snapshot:
+      state: present
+      db_instance_identifier: "{{ instance_id }}"
+      db_snapshot_identifier: "{{ snapshot_id }}-b"
+      wait: true
+    register: _result_instance_snapshot
+
+  - assert:
+      that:
+      - _result_instance_snapshot.changed
+      - "'availability_zone' in _result_instance_snapshot"
+      - "'instance_create_time' in _result_instance_snapshot"
+      - "'db_instance_identifier' in _result_instance_snapshot"
+      - _result_instance_snapshot.db_instance_identifier == "{{ instance_id }}"
+      - "'db_snapshot_identifier' in _result_instance_snapshot"
+      - _result_instance_snapshot.db_snapshot_identifier == "{{ snapshot_id }}-b"
+      - "'db_snapshot_arn' in _result_instance_snapshot"
+      - "'dbi_resource_id' in _result_instance_snapshot"
+      - "'encrypted' in _result_instance_snapshot"
+      - "'engine' in _result_instance_snapshot"
+      - _result_instance_snapshot.engine == "{{ engine }}"
+      - "'engine_version' in _result_instance_snapshot"
+      - _result_instance_snapshot.engine_version == "{{ mariadb_engine_version }}"
+      - "'iam_database_authentication_enabled' in _result_instance_snapshot"
+      - "'license_model' in _result_instance_snapshot"
+      - "'master_username' in _result_instance_snapshot"
+      - _result_instance_snapshot.master_username == "{{ username }}"
+      - "'snapshot_create_time' in _result_instance_snapshot"
+      - "'snapshot_type' in _result_instance_snapshot"
+      - "'status' in _result_instance_snapshot"
+      - _result_instance_snapshot.status == "available"
+      - "'snapshot_type' in _result_instance_snapshot"
+      - _result_instance_snapshot.snapshot_type == "manual"
+      - "'status' in _result_instance_snapshot"
+      - "'storage_type' in _result_instance_snapshot"
+      - _result_instance_snapshot.storage_type == "gp2"
+      - "'tags' in _result_instance_snapshot"
+      - "'vpc_id' in _result_instance_snapshot"
+
+  - name: Get all snapshots for the existing RDS instance
+    rds_snapshot_info:
+      db_instance_identifier: "{{ instance_id }}"
+    register: _result_instance_snapshot_info
+
+  - assert:
+      that:
+      - _result_instance_snapshot_info is successful
+      #- _result_instance_snapshot_info.cluster_snapshots | length == 3
+
+  - name: Delete existing DB instance snapshot (CHECK_MODE)
+    rds_instance_snapshot:
+      state: absent
+      db_snapshot_identifier: "{{ snapshot_id }}-b"
+    register: _result_delete_snapshot
+    check_mode: yes
+
+  - assert:
+      that:
+      - _result_delete_snapshot.changed
+
+  - name: Delete the existing DB instance snapshot
+    rds_instance_snapshot:
+      state: absent
+      db_snapshot_identifier: "{{ snapshot_id }}-b"
+    register: _result_delete_snapshot
+
+  - assert:
+      that:
+      - _result_delete_snapshot.changed
+
+  - name: Delete existing DB instance snapshot (CHECK_MODE - IDEMPOTENCE)
+    rds_instance_snapshot:
+      state: absent
+      db_snapshot_identifier: "{{ snapshot_id }}-b"
+    register: _result_delete_snapshot
+    check_mode: yes
+
+  - assert:
+      that:
+      - not _result_delete_snapshot.changed
+
+  - name: Delete the existing DB instance snapshot (IDEMPOTENCE)
+    rds_instance_snapshot:
+      state: absent
+      db_snapshot_identifier: "{{ snapshot_id }}-b"
+    register: _result_delete_snapshot
+
+  - assert:
+      that:
+      - not _result_delete_snapshot.changed
+
+  - name: Take another snapshot of the existing RDS instance and assign tags
+    rds_instance_snapshot:
+      state: present
+      db_instance_identifier: "{{ instance_id }}"
+      db_snapshot_identifier: "{{ snapshot_id }}-b"
+      wait: true
+      tags:
+        tag_one: '{{ snapshot_id }}-b One'
+        "Tag Two": 'two {{ snapshot_id }}-b'
+    register: _result_instance_snapshot
+
+  - assert:
+      that:
+      - _result_instance_snapshot.changed
+      - "'availability_zone' in _result_instance_snapshot"
+      - "'instance_create_time' in _result_instance_snapshot"
+      - "'db_instance_identifier' in _result_instance_snapshot"
+      - _result_instance_snapshot.db_instance_identifier == "{{ instance_id }}"
+      - "'db_snapshot_identifier' in _result_instance_snapshot"
+      - _result_instance_snapshot.db_snapshot_identifier == "{{ snapshot_id }}-b"
+      - "'db_snapshot_arn' in _result_instance_snapshot"
+      - "'dbi_resource_id' in _result_instance_snapshot"
+      - "'encrypted' in _result_instance_snapshot"
+      - "'engine' in _result_instance_snapshot"
+      - _result_instance_snapshot.engine == "{{ engine }}"
+      - "'engine_version' in _result_instance_snapshot"
+      - _result_instance_snapshot.engine_version == "{{ mariadb_engine_version }}"
+      - "'iam_database_authentication_enabled' in _result_instance_snapshot"
+      - "'license_model' in _result_instance_snapshot"
+      - "'master_username' in _result_instance_snapshot"
+      - _result_instance_snapshot.master_username == "{{ username }}"
+      - "'snapshot_create_time' in _result_instance_snapshot"
+      - "'snapshot_type' in _result_instance_snapshot"
+      - "'status' in _result_instance_snapshot"
+      - _result_instance_snapshot.status == "available"
+      - "'snapshot_type' in _result_instance_snapshot"
+      - _result_instance_snapshot.snapshot_type == "manual"
+      - "'status' in _result_instance_snapshot"
+      - "'storage_type' in _result_instance_snapshot"
+      - _result_instance_snapshot.storage_type == "gp2"
+      - "'tags' in _result_instance_snapshot"
+      - _result_instance_snapshot.tags | length == 2
+      - _result_instance_snapshot.tags["tag_one"] == "{{ snapshot_id }}-b One"
+      - _result_instance_snapshot.tags["Tag Two"] == "two {{ snapshot_id }}-b"
+      - "'vpc_id' in _result_instance_snapshot"
+
+  - name: Attempt to take another snapshot of the existing RDS instance and assign tags (idempotence)
+    rds_instance_snapshot:
+      state: present
+      db_instance_identifier: "{{ instance_id }}"
+      db_snapshot_identifier: "{{ snapshot_id }}-b"
+      wait: true
+      tags:
+        tag_one: '{{ snapshot_id }}-b One'
+        "Tag Two": 'two {{ snapshot_id }}-b'
+    register: _result_instance_snapshot
+
+  - assert:
+      that:
+      - not _result_instance_snapshot.changed
+
+  - name: Take another snapshot of the existing RDS instance and update tags
+    rds_instance_snapshot:
+      state: present
+      db_instance_identifier: "{{ instance_id }}"
+      db_snapshot_identifier: "{{ snapshot_id }}-b"
+      tags:
+        tag_three: '{{ snapshot_id }}-b Three'
+        "Tag Two": 'two {{ snapshot_id }}-b'
+    register: _result_instance_snapshot
+
+  - assert:
+      that:
+      - _result_instance_snapshot.changed
+      - "'availability_zone' in _result_instance_snapshot"
+      - "'instance_create_time' in _result_instance_snapshot"
+      - "'db_instance_identifier' in _result_instance_snapshot"
+      - _result_instance_snapshot.db_instance_identifier == "{{ instance_id }}"
+      - "'db_snapshot_identifier' in _result_instance_snapshot"
+      - _result_instance_snapshot.db_snapshot_identifier == "{{ snapshot_id }}-b"
+      - "'db_snapshot_arn' in _result_instance_snapshot"
+      - "'dbi_resource_id' in _result_instance_snapshot"
+      - "'encrypted' in _result_instance_snapshot"
+      - "'engine' in _result_instance_snapshot"
+      - _result_instance_snapshot.engine == "{{ engine }}"
+      - "'engine_version' in _result_instance_snapshot"
+      - _result_instance_snapshot.engine_version == "{{ mariadb_engine_version }}"
+      - "'iam_database_authentication_enabled' in _result_instance_snapshot"
+      - "'license_model' in _result_instance_snapshot"
+      - "'master_username' in _result_instance_snapshot"
+      - _result_instance_snapshot.master_username == "{{ username }}"
+      - "'snapshot_create_time' in _result_instance_snapshot"
+      - "'snapshot_type' in _result_instance_snapshot"
+      - "'status' in _result_instance_snapshot"
+      - _result_instance_snapshot.status == "available"
+      - "'snapshot_type' in _result_instance_snapshot"
+      - _result_instance_snapshot.snapshot_type == "manual"
+      - "'status' in _result_instance_snapshot"
+      - "'storage_type' in _result_instance_snapshot"
+      - _result_instance_snapshot.storage_type == "gp2"
+      - "'tags' in _result_instance_snapshot"
+      - _result_instance_snapshot.tags | length == 2
+      - _result_instance_snapshot.tags["tag_three"] == "{{ snapshot_id }}-b Three"
+      - _result_instance_snapshot.tags["Tag Two"] == "two {{ snapshot_id }}-b"
+      - "'vpc_id' in _result_instance_snapshot"
+
+  - name: Take another snapshot of the existing RDS instance and update tags without purge
+    rds_instance_snapshot:
+      state: present
+      db_instance_identifier: "{{ instance_id }}"
+      db_snapshot_identifier: "{{ snapshot_id }}-b"
+      purge_tags: no
+      tags:
+        tag_one: '{{ snapshot_id }}-b One'
+    register: _result_instance_snapshot
+
+  - assert:
+      that:
+      - _result_instance_snapshot.changed
+      - "'availability_zone' in _result_instance_snapshot"
+      - "'instance_create_time' in _result_instance_snapshot"
+      - "'db_instance_identifier' in _result_instance_snapshot"
+      - _result_instance_snapshot.db_instance_identifier == "{{ instance_id }}"
+      - "'db_snapshot_identifier' in _result_instance_snapshot"
+      - _result_instance_snapshot.db_snapshot_identifier == "{{ snapshot_id }}-b"
+      - "'db_snapshot_arn' in _result_instance_snapshot"
+      - "'dbi_resource_id' in _result_instance_snapshot"
+      - "'encrypted' in _result_instance_snapshot"
+      - "'engine' in _result_instance_snapshot"
+      - _result_instance_snapshot.engine == "{{ engine }}"
+      - "'engine_version' in _result_instance_snapshot"
+      - _result_instance_snapshot.engine_version == "{{ mariadb_engine_version }}"
+      - "'iam_database_authentication_enabled' in _result_instance_snapshot"
+      - "'license_model' in _result_instance_snapshot"
+      - "'master_username' in _result_instance_snapshot"
+      - _result_instance_snapshot.master_username == "{{ username }}"
+      - "'snapshot_create_time' in _result_instance_snapshot"
+      - "'snapshot_type' in _result_instance_snapshot"
+      - "'status' in _result_instance_snapshot"
+      - _result_instance_snapshot.status == "available"
+      - "'snapshot_type' in _result_instance_snapshot"
+      - _result_instance_snapshot.snapshot_type == "manual"
+      - "'status' in _result_instance_snapshot"
+      - "'storage_type' in _result_instance_snapshot"
+      - _result_instance_snapshot.storage_type == "gp2"
+      - "'tags' in _result_instance_snapshot"
+      - _result_instance_snapshot.tags | length == 3
+      - _result_instance_snapshot.tags["tag_one"] == "{{ snapshot_id }}-b One"
+      - _result_instance_snapshot.tags["Tag Two"] == "two {{ snapshot_id }}-b"
+      - _result_instance_snapshot.tags["tag_three"] == "{{ snapshot_id }}-b Three"
+      - "'vpc_id' in _result_instance_snapshot"
+
+  - name: Take another snapshot of the existing RDS instance and do not specify any tag to ensure previous tags are not removed
+    rds_instance_snapshot:
+      state: present
+      db_instance_identifier: "{{ instance_id }}"
+      db_snapshot_identifier: "{{ snapshot_id }}-b"
+    register: _result_instance_snapshot
+
+  - assert:
+      that:
+      - not _result_instance_snapshot.changed
+
+  # ------------------------------------------------------------------------------------------
+  # Test copying a snapshot
+  ### Note - copying a snapshot from a different region is supported, but not in CI runs,
+  ### because the aws-terminator only terminates resources in one region.
+
+  - set_fact:
+      _snapshot_arn: "{{ _result_instance_snapshot.db_snapshot_arn }}"
+
+  - name: Copy a snapshot (check mode)
+    rds_instance_snapshot:
+      id: "{{ snapshot_id }}-copy"
+      source_id: "{{ snapshot_id }}-b"
+      copy_tags: yes
+      wait: true
+    register: _result_instance_snapshot
+    check_mode: yes
+
+  - assert:
+      that:
+        - _result_instance_snapshot.changed
+
+  - name: Copy a snapshot
+    rds_instance_snapshot:
+      id: "{{ snapshot_id }}-copy"
+      source_id: "{{ snapshot_id }}-b"
+      copy_tags: yes
+      wait: true
+    register: _result_instance_snapshot
+
+  - assert:
+      that:
+        - _result_instance_snapshot.changed
+        - _result_instance_snapshot.db_instance_identifier == "{{ instance_id }}"
+        - _result_instance_snapshot.source_db_snapshot_identifier == "{{ _snapshot_arn }}"
+        - _result_instance_snapshot.db_snapshot_identifier == "{{ snapshot_id }}-copy"
+        - "'tags' in _result_instance_snapshot"
+        - _result_instance_snapshot.tags | length == 3
+        - _result_instance_snapshot.tags["tag_one"] == "{{ snapshot_id }}-b One"
+        - _result_instance_snapshot.tags["Tag Two"] == "two {{ snapshot_id }}-b"
+        - _result_instance_snapshot.tags["tag_three"] == "{{ snapshot_id }}-b Three"
+
+  - name: Copy a snapshot (idempotence - check mode)
+    rds_instance_snapshot:
+      id: "{{ snapshot_id }}-copy"
+      source_id: "{{ snapshot_id }}-b"
+      copy_tags: yes
+      wait: true
+    register: _result_instance_snapshot
+    check_mode: yes
+
+  - assert:
+      that:
+        - not _result_instance_snapshot.changed
+
+  - name: Copy a snapshot (idempotence)
+    rds_instance_snapshot:
+      id: "{{ snapshot_id }}-copy"
+      source_id: "{{ snapshot_id }}-b"
+      copy_tags: yes
+      wait: true
+    register: _result_instance_snapshot
+
+  - assert:
+      that:
+        - not _result_instance_snapshot.changed
+        - _result_instance_snapshot.db_instance_identifier == "{{ instance_id }}"
+        - _result_instance_snapshot.source_db_snapshot_identifier == "{{ _snapshot_arn }}"
+        - _result_instance_snapshot.db_snapshot_identifier == "{{ snapshot_id }}-copy"
+        - "'tags' in _result_instance_snapshot"
+        - _result_instance_snapshot.tags | length == 3
+        - _result_instance_snapshot.tags["tag_one"] == "{{ snapshot_id }}-b One"
+        - _result_instance_snapshot.tags["Tag Two"] == "two {{ snapshot_id }}-b"
+        - _result_instance_snapshot.tags["tag_three"] == "{{ snapshot_id }}-b Three"
+
+  always:
+  - name: Delete the existing DB instance snapshots
+    rds_instance_snapshot:
+      state: absent
+      db_snapshot_identifier: "{{ item }}"
+      wait: false
+    register: _result_delete_snapshot
+    ignore_errors: true
+    loop:
+    - "{{ snapshot_id }}"
+    - "{{ snapshot_id }}-b"
+    - "{{ snapshot_id }}-copy"
+
+  - name: Delete the existing RDS instance without creating a final snapshot
+    rds_instance:
+      state: absent
+      instance_id: "{{ instance_id }}"
+      skip_final_snapshot: True
+      wait: false
+    register: _result_delete_instance
+    ignore_errors: true
diff --git a/tests/integration/targets/rds_instance_snapshot/vars/main.yml b/tests/integration/targets/rds_instance_snapshot/vars/main.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/rds_instance_snapshot/vars/main.yml
@@ -0,0 +1 @@
+---