Repository: ansible-collections/community.aws

Issue:
Extend rds module to allow copying of rds snaphots cross region
<!--- Verify first that your feature was not already discussed on GitHub -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Describe the new feature/improvement briefly below -->
Extend to allow module to copy an rds snapshot to another region.

##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
<!--- Write the short name of the module, plugin, task or feature below, use your best guess if unsure -->
rds_instance
rds
##### ADDITIONAL INFORMATION
<!--- Describe how the feature would be used, why it is needed and what it would solve -->
During automated backups of databases, copy rds snapshot to different region for backup redundancy and ensure availability of rds instance.

<!--- Paste example playbooks or commands between quotes below -->
```yaml

```

<!--- HINT: You can also paste gist.github.com links for larger files -->



Discussion:
Files identified in the description:
* [`plugins/modules/rds.py`](https://github.com/ansible-collections/community.aws/blob/main/plugins/modules/rds.py)
* [`plugins/modules/rds_instance.py`](https://github.com/ansible-collections/community.aws/blob/main/plugins/modules/rds_instance.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @jillr @s-hertel @tremble @willthames @wimnat
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Files identified in the description:
* [`plugins/modules/rds_instance.py`](https://github.com/['ansible-collections/amazon.aws', 'ansible-collections/community.aws', 'ansible-collections/community.vmware']/blob/main/plugins/modules/rds_instance.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @markuman
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@markuman @alinabuzachis I would assume this functionality should be added to `rds_instance_snapshot` ?
> @markuman @alinabuzachis I would assume this functionality should be added to `rds_instance_snapshot` ?

Sounds good to me.
Added PR for this!

Resolving patch:
diff --git a/plugins/modules/rds_instance_snapshot.py b/plugins/modules/rds_instance_snapshot.py
--- a/plugins/modules/rds_instance_snapshot.py
+++ b/plugins/modules/rds_instance_snapshot.py
@@ -32,15 +32,37 @@
     type: str
   db_instance_identifier:
     description:
-      - Database instance identifier. Required when state is present.
+      - 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: 'no'
+    default: False
   wait_timeout:
     description:
       - how long before wait gives up, in seconds.
@@ -52,13 +74,14 @@
     type: dict
   purge_tags:
     description:
-      - whether to remove tags not present in the C(tags) parameter.
+      - whether to remove tags not present in the I(tags) parameter.
     default: True
     type: bool
 author:
     - "Will Thames (@willthames)"
     - "Michael De La Rue (@mikedlr)"
     - "Alina Buzachis (@alinabuzachis)"
+    - "Joseph Torcasso (@jatorcasso)"
 extends_documentation_fragment:
 - amazon.aws.aws
 - amazon.aws.ec2
@@ -70,6 +93,15 @@
   community.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
+  community.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: yes
 
 - name: Delete snapshot
   community.aws.rds_instance_snapshot:
@@ -163,6 +195,12 @@
   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
@@ -202,31 +240,41 @@
 
 # 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.ec2 import ansible_dict_to_boto3_tag_list
-from ansible_collections.amazon.aws.plugins.module_utils.rds import get_tags
-from ansible_collections.amazon.aws.plugins.module_utils.rds import ensure_tags
+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:
-        response = client.describe_db_snapshots(DBSnapshotIdentifier=snapshot_id)
-    except is_boto3_error_code("DBSnapshotNotFoundFault"):
-        return None
-    except is_boto3_error_code("DBSnapshotNotFound"):  # pylint: disable=duplicate-except
-        return None
+        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 response['DBSnapshots'][0]
+    return snapshot
 
 
-def fetch_tags(snapshot):
-    snapshot["Tags"] = get_tags(client, module, snapshot["DBSnapshotArn"])
+def get_parameters(parameters, method_name):
+    if method_name == 'copy_db_snapshot':
+        parameters['TargetDBSnapshotIdentifier'] = module.params['db_snapshot_identifier']
 
-    return camel_dict_to_snake_dict(snapshot, ignore_list=["Tags"])
+    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():
@@ -236,40 +284,68 @@ def ensure_snapshot_absent():
 
     snapshot = get_snapshot(snapshot_name)
     if not snapshot:
-        return dict(changed=changed)
+        module.exit_json(changed=changed)
     elif snapshot and snapshot["Status"] != "deleting":
         snapshot, changed = call_method(client, module, "delete_db_snapshot", params)
 
-    return dict(changed=changed)
+    module.exit_json(changed=changed)
 
 
-def ensure_snapshot_present():
-    db_instance_identifier = module.params.get('db_instance_identifier')
+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:
-        params = {
-            "DBSnapshotIdentifier": snapshot_name,
-            "DBInstanceIdentifier": db_instance_identifier
-        }
-        if module.params.get("tags"):
-            params['Tags'] = ansible_dict_to_boto3_tag_list(module.params.get("tags"))
-        _result, changed = call_method(client, module, "create_db_snapshot", params)
+        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)
 
-        if module.check_mode:
-            return dict(changed=changed)
+    return changed
 
-        return dict(changed=changed, **fetch_tags(get_snapshot(snapshot_name)))
 
-    existing_tags = get_tags(client, module, snapshot["DBSnapshotArn"])
-    changed |= ensure_tags(client, module, snapshot["DBSnapshotArn"], existing_tags,
-                           module.params["tags"], module.params["purge_tags"])
+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.check_mode:
-        return dict(changed=changed)
+    if module.params.get('tags'):
+        changed |= ensure_tags(client, module, snapshot['DBSnapshotArn'], snapshot['Tags'], module.params['tags'], module.params['purge_tags'])
 
-    return dict(changed=changed, **fetch_tags(get_snapshot(snapshot_name)))
+    return changed
 
 
 def main():
@@ -280,16 +356,18 @@ def main():
         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'),
         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,
-        required_if=[['state', 'present', ['db_instance_identifier']]],
-        supports_check_mode=True,
+        supports_check_mode=True
     )
 
     retry_decorator = AWSRetry.jittered_backoff(retries=10)
@@ -298,12 +376,13 @@ def main():
     except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
         module.fail_json_aws(e, msg="Failed to connect to AWS.")
 
-    if module.params['state'] == 'absent':
-        ret_dict = ensure_snapshot_absent()
-    else:
-        ret_dict = ensure_snapshot_present()
+    state = module.params.get("state")
+    if state == 'absent':
+        ensure_snapshot_absent()
 
-    module.exit_json(**ret_dict)
+    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__':


Test patch:
diff --git a/tests/integration/targets/rds_instance_snapshot/defaults/main.yml b/tests/integration/targets/rds_instance_snapshot/defaults/main.yml
--- a/tests/integration/targets/rds_instance_snapshot/defaults/main.yml
+++ b/tests/integration/targets/rds_instance_snapshot/defaults/main.yml
@@ -2,13 +2,13 @@
 # defaults file for rds_instance_snapshot
 
 # Create RDS instance
-instance_id: 'ansible-test-instance-{{ resource_prefix }}'
+instance_id: '{{ resource_prefix }}-instance'
 username: 'testrdsusername'
-password: 'test-rds_password'
+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: 'ansible-test-instance-snapshot-{{ resource_prefix }}'
+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
--- a/tests/integration/targets/rds_instance_snapshot/tasks/main.yml
+++ b/tests/integration/targets/rds_instance_snapshot/tasks/main.yml
@@ -27,12 +27,12 @@
       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
@@ -49,7 +49,7 @@
   - assert:
       that:
       - _result_instance_snapshot.changed
-  
+
   - name: Take a snapshot of the existing RDS instance
     rds_instance_snapshot:
       state: present
@@ -89,18 +89,70 @@
       - _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
@@ -140,38 +192,59 @@
       - _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
@@ -217,7 +290,7 @@
       - _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
@@ -232,7 +305,7 @@
   - assert:
       that:
       - not _result_instance_snapshot.changed
-  
+
   - name: Take another snapshot of the existing RDS instance and update tags
     rds_instance_snapshot:
       state: present
@@ -277,7 +350,7 @@
       - _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
@@ -323,7 +396,7 @@
       - _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
@@ -334,25 +407,99 @@
   - assert:
       that:
       - not _result_instance_snapshot.changed
-  
-  always:  
+
+  # ------------------------------------------------------------------------------------------
+  # 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: "{{ item }}"
+      instance_id: "{{ instance_id }}"
       skip_final_snapshot: True
+      wait: false
     register: _result_delete_instance
     ignore_errors: true
-    loop:
-    - "{{ instance_id }}"
-    - "{{ instance_id }}-b"