Repository: ansible/ansible

Issue:
route53_zone ignores 'comment' parameter if zone already exists
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
route53_zone

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.4.2.0
python version = 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04) [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)]
```

##### CONFIGURATION
ANSIBLE_PIPELINING(/###/ansible.cfg) = True
DEFAULT_MODULE_LANG(/###/ansible.cfg) = en_US.UTF-8
DEFAULT_VAULT_PASSWORD_FILE(env: ANSIBLE_VAULT_PASSWORD_FILE) = /###/.vault_password

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say "N/A" for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.
-->
MacOS 10.13.2

##### SUMMARY
Ansible will only update the AWS zone comment with value of the route53_zone 'comment' parameter if the zone is being created. If the zone already exists, 'comment' is ignored and the task returns 'ok'

##### STEPS TO REPRODUCE
Run route53_zone against an existing AWS zone, where "comment" parameter differs from the current AWS zone comment.

##### EXPECTED RESULTS
AWS zone comment should be updated with the route53_zone comment parameter value passed

##### ACTUAL RESULTS
route53_zone comment parameter value is ignored.

Task to create a new zone:

<!--- Paste verbatim command output between quotes below -->
```yaml
- route53_zone:
    zone: example.com
    comment: the new zone will be created with this comment
```
Zone is created, ansible returns
```
changed: [localhost]
```
Alter the comment parameter and re-run the task:
```yaml
- route53_zone:
    zone: example.com
    comment: foo bar baz
```
Task returns OK, AWS zone remains unchanged
```
ok: [localhost]
```



Discussion:
Files identified in the description:
* [lib/ansible/modules/cloud/amazon/route53_zone.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/cloud/amazon/route53_zone.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 @minichate @willthames
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/cloud/amazon/route53_zone.py b/lib/ansible/modules/cloud/amazon/route53_zone.py
--- a/lib/ansible/modules/cloud/amazon/route53_zone.py
+++ b/lib/ansible/modules/cloud/amazon/route53_zone.py
@@ -25,6 +25,7 @@
 description:
     - Creates and deletes Route53 private and public zones
 version_added: "2.0"
+requirements: [ boto3 ]
 options:
     zone:
         description:
@@ -116,34 +117,35 @@
     sample: "Z6JQG9820BEFMW"
 '''
 
+import time
+from ansible.module_utils.aws.core import AnsibleAWSModule
+from ansible.module_utils.ec2 import boto3_conn, ec2_argument_spec, get_aws_connection_info
+
 try:
-    import boto
-    from boto.route53 import Route53Connection
-    from boto.route53.zone import Zone
-    HAS_BOTO = True
+    from botocore.exceptions import BotoCoreError, ClientError
 except ImportError:
-    HAS_BOTO = False
-
-from ansible.module_utils.basic import AnsibleModule
-from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info
+    pass  # handled by AnsibleAWSModule
 
 
-def find_zones(conn, zone_in, private_zone):
-    results = conn.get_all_hosted_zones()
-    zones = {}
-    for r53zone in results['ListHostedZonesResponse']['HostedZones']:
+def find_zones(module, client, zone_in, private_zone):
+    try:
+        paginator = client.get_paginator('list_hosted_zones')
+        results = paginator.paginate().build_full_result()
+    except (BotoCoreError, ClientError) as e:
+        module.fail_json_aws(e, msg="Could not list current hosted zones")
+    zones = []
+    for r53zone in results['HostedZones']:
         if r53zone['Name'] != zone_in:
             continue
         # only save zone names that match the public/private setting
-        if r53zone['Config']['PrivateZone'] == 'true' and private_zone:
-            zones[r53zone.get('Id', '').replace('/hostedzone/', '')] = r53zone['Name']
-        if r53zone['Config']['PrivateZone'] == 'false' and not private_zone:
-            zones[r53zone.get('Id', '').replace('/hostedzone/', '')] = r53zone['Name']
+        if (r53zone['Config']['PrivateZone'] and private_zone) or \
+           (not r53zone['Config']['PrivateZone'] and not private_zone):
+            zones.append(r53zone)
 
     return zones
 
 
-def create(conn, module, matching_zones):
+def create(module, client, matching_zones):
     zone_in = module.params.get('zone').lower()
     vpc_id = module.params.get('vpc_id')
     vpc_region = module.params.get('vpc_region')
@@ -159,55 +161,105 @@ def create(conn, module, matching_zones):
         'vpc_id': vpc_id,
         'vpc_region': vpc_region,
         'comment': comment,
+        'name': zone_in,
     }
 
     if private_zone:
-        changed, result = create_private(conn, matching_zones, vpc_id, vpc_region, zone_in, record)
+        changed, result = create_or_update_private(module, client, matching_zones, record)
     else:
-        changed, result = create_public(conn, matching_zones, zone_in, record)
+        changed, result = create_or_update_public(module, client, matching_zones, record)
 
     return changed, result
 
 
-def create_private(conn, matching_zones, vpc_id, vpc_region, zone_in, record):
+def create_or_update_private(module, client, matching_zones, record):
     for z in matching_zones:
-        zone_details = conn.get_hosted_zone(z)['GetHostedZoneResponse']  # could be in different regions or have different VPCids
+        try:
+            result = client.get_hosted_zone(Id=z['Id'])  # could be in different regions or have different VPCids
+        except (BotoCoreError, ClientError) as e:
+            module.fail_json_aws(e, msg="Could not get details about hosted zone %s" % z['Id'])
+        zone_details = result['HostedZone']
+        vpc_details = result['VPCs']
         current_vpc_id = None
         current_vpc_region = None
-        if isinstance(zone_details['VPCs'], dict):
-            if zone_details['VPCs']['VPC']['VPCId'] == vpc_id:
-                current_vpc_id = zone_details['VPCs']['VPC']['VPCId']
-                current_vpc_region = zone_details['VPCs']['VPC']['VPCRegion']
+        if isinstance(vpc_details, dict):
+            if vpc_details['VPC']['VPCId'] == record['vpc_id']:
+                current_vpc_id = vpc_details['VPC']['VPCId']
+                current_vpc_region = vpc_details['VPC']['VPCRegion']
         else:
-            if vpc_id in [v['VPCId'] for v in zone_details['VPCs']]:
-                current_vpc_id = vpc_id
-                if vpc_region in [v['VPCRegion'] for v in zone_details['VPCs']]:
-                    current_vpc_region = vpc_region
-        if vpc_id == current_vpc_id and vpc_region == current_vpc_region:
-            record['zone_id'] = z
-            record['name'] = zone_in
-            record['msg'] = "There is already a private hosted zone in the same region with the same VPC \
-                you chose. Unable to create a new private hosted zone in the same name space."
-            changed = False
-            return changed, record
+            if record['vpc_id'] in [v['VPCId'] for v in vpc_details]:
+                current_vpc_id = record['vpc_id']
+                if record['vpc_region'] in [v['VPCRegion'] for v in vpc_details]:
+                    current_vpc_region = record['vpc_region']
+
+        if record['vpc_id'] == current_vpc_id and record['vpc_region'] == current_vpc_region:
+            record['zone_id'] = zone_details['Id'].replace('/hostedzone/', '')
+            if 'Comment' in zone_details['Config'] and zone_details['Config']['Comment'] != record['comment']:
+                try:
+                    client.update_hosted_zone_comment(Id=zone_details['Id'], Comment=record['comment'])
+                except (BotoCoreError, ClientError) as e:
+                    module.fail_json_aws(e, msg="Could not update comment for hosted zone %s" % zone_details['Id'])
+                return True, record
+            else:
+                record['msg'] = "There is already a private hosted zone in the same region with the same VPC \
+                    you chose. Unable to create a new private hosted zone in the same name space."
+                return False, record
 
-    result = conn.create_hosted_zone(zone_in, **record)
-    hosted_zone = result['CreateHostedZoneResponse']['HostedZone']
+    try:
+        result = client.create_hosted_zone(
+            Name=record['name'],
+            HostedZoneConfig={
+                'Comment': record['comment'] if record['comment'] is not None else "",
+                'PrivateZone': True,
+            },
+            VPC={
+                'VPCRegion': record['vpc_region'],
+                'VPCId': record['vpc_id'],
+            },
+            CallerReference="%s-%s" % (record['name'], time.time()),
+        )
+    except (BotoCoreError, ClientError) as e:
+        module.fail_json_aws(e, msg="Could not create hosted zone")
+    hosted_zone = result['HostedZone']
     zone_id = hosted_zone['Id'].replace('/hostedzone/', '')
     record['zone_id'] = zone_id
-    record['name'] = zone_in
     changed = True
     return changed, record
 
 
-def create_public(conn, matching_zones, zone_in, record):
-    if zone_in in matching_zones.values():
-        zone_details = conn.get_hosted_zone(
-            list(matching_zones)[0])['GetHostedZoneResponse']['HostedZone']
-        changed = False
-    else:
-        result = conn.create_hosted_zone(zone_in, **record)
-        zone_details = result['CreateHostedZoneResponse']['HostedZone']
+def create_or_update_public(module, client, matching_zones, record):
+    zone_details = None
+    for matching_zone in matching_zones:
+        try:
+            zone_details = client.get_hosted_zone(Id=matching_zone['Id'])['HostedZone']
+        except (BotoCoreError, ClientError) as e:
+            module.fail_json_aws(e, msg="Could not get details about hosted zone %s" % matching_zone['Id'])
+        if 'Comment' in zone_details['Config'] and zone_details['Config']['Comment'] != record['comment']:
+            try:
+                client.update_hosted_zone_comment(
+                    Id=zone_details['Id'],
+                    Comment=record['comment']
+                )
+            except (BotoCoreError, ClientError) as e:
+                module.fail_json_aws(e, msg="Could not update comment for hosted zone %s" % zone_details['Id'])
+            changed = True
+        else:
+            changed = False
+        break
+
+    if zone_details is None:
+        try:
+            result = client.create_hosted_zone(
+                Name=record['name'],
+                HostedZoneConfig={
+                    'Comment': record['comment'] if record['comment'] is not None else "",
+                    'PrivateZone': False,
+                },
+                CallerReference="%s-%s" % (record['name'], time.time())
+            )
+        except (BotoCoreError, ClientError) as e:
+            module.fail_json_aws(e, msg="Could not create hosted zone")
+        zone_details = result['HostedZone']
         changed = True
 
     record['zone_id'] = zone_details['Id'].replace('/hostedzone/', '')
@@ -216,54 +268,62 @@ def create_public(conn, matching_zones, zone_in, record):
     return changed, record
 
 
-def delete_private(conn, matching_zones, vpc_id, vpc_region):
-    changed = False
+def delete_private(module, client, matching_zones, vpc_id, vpc_region):
     for z in matching_zones:
-        zone_details = conn.get_hosted_zone(z)['GetHostedZoneResponse']
-        if isinstance(zone_details['VPCs'], dict):
-            if zone_details['VPCs']['VPC']['VPCId'] == vpc_id and vpc_region == zone_details['VPCs']['VPC']['VPCRegion']:
-                conn.delete_hosted_zone(z)
-                changed = True
-                msg = "Successfully deleted %s" % matching_zones[z]
-                break
-            else:
-                changed = False
+        try:
+            result = client.get_hosted_zone(Id=z['Id'])
+        except (BotoCoreError, ClientError) as e:
+            module.fail_json_aws(e, msg="Could not get details about hosted zone %s" % z['Id'])
+        zone_details = result['HostedZone']
+        vpc_details = result['VPCs']
+        if isinstance(vpc_details, dict):
+            if vpc_details['VPC']['VPCId'] == vpc_id and vpc_region == vpc_details['VPC']['VPCRegion']:
+                try:
+                    client.delete_hosted_zone(Id=z['Id'])
+                except (BotoCoreError, ClientError) as e:
+                    module.fail_json_aws(e, msg="Could not delete hosted zone %s" % z['Id'])
+                return True, "Successfully deleted %s" % zone_details['Name']
         else:
-            if vpc_id in [v['VPCId'] for v in zone_details['VPCs']] and vpc_region in [v['VPCRegion'] for v in zone_details['VPCs']]:
-                conn.delete_hosted_zone(z)
-                changed = True
-                msg = "Successfully deleted %s" % matching_zones[z]
-                break
-            else:
-                changed = False
-    if not changed:
-        msg = "The vpc_id and the vpc_region do not match a private hosted zone."
+            if vpc_id in [v['VPCId'] for v in vpc_details] and vpc_region in [v['VPCRegion'] for v in vpc_details]:
+                try:
+                    client.delete_hosted_zone(Id=z['Id'])
+                except (BotoCoreError, ClientError) as e:
+                    module.fail_json_aws(e, msg="Could not delete hosted zone %s" % z['Id'])
+                return True, "Successfully deleted %s" % zone_details['Name']
 
-    return changed, msg
+    return False, "The vpc_id and the vpc_region do not match a private hosted zone."
 
 
-def delete_public(conn, matching_zones):
+def delete_public(module, client, matching_zones):
     if len(matching_zones) > 1:
         changed = False
         msg = "There are multiple zones that match. Use hosted_zone_id to specify the correct zone."
     else:
-        for z in matching_zones:
-            conn.delete_hosted_zone(z)
-            changed = True
-            msg = "Successfully deleted %s" % matching_zones[z]
+        try:
+            client.delete_hosted_zone(Id=matching_zones[0]['Id'])
+        except (BotoCoreError, ClientError) as e:
+            module.fail_json_aws(e, msg="Could not get delete hosted zone %s" % matching_zones[0]['Id'])
+        changed = True
+        msg = "Successfully deleted %s" % matching_zones[0]['Id']
     return changed, msg
 
 
-def delete_hosted_id(conn, hosted_zone_id, matching_zones):
+def delete_hosted_id(module, client, hosted_zone_id, matching_zones):
     if hosted_zone_id == "all":
         deleted = []
         for z in matching_zones:
-            deleted.append(z)
-            conn.delete_hosted_zone(z)
+            deleted.append(z['Id'])
+            try:
+                client.delete_hosted_zone(Id=z['Id'])
+            except (BotoCoreError, ClientError) as e:
+                module.fail_json_aws(e, msg="Could not delete hosted zone %s" % z['Id'])
         changed = True
         msg = "Successfully deleted zones: %s" % deleted
-    elif hosted_zone_id in matching_zones:
-        conn.delete_hosted_zone(hosted_zone_id)
+    elif hosted_zone_id in [zo['Id'].replace('/hostedzone/', '') for zo in matching_zones]:
+        try:
+            client.delete_hosted_zone(Id=hosted_zone_id)
+        except (BotoCoreError, ClientError) as e:
+            module.fail_json_aws(e, msg="Could not delete hosted zone %s" % hosted_zone_id)
         changed = True
         msg = "Successfully deleted zone: %s" % hosted_zone_id
     else:
@@ -272,11 +332,10 @@ def delete_hosted_id(conn, hosted_zone_id, matching_zones):
     return changed, msg
 
 
-def delete(conn, module, matching_zones):
+def delete(module, client, matching_zones):
     zone_in = module.params.get('zone').lower()
     vpc_id = module.params.get('vpc_id')
     vpc_region = module.params.get('vpc_region')
-    comment = module.params.get('comment')
     hosted_zone_id = module.params.get('hosted_zone_id')
 
     if not zone_in.endswith('.'):
@@ -284,14 +343,14 @@ def delete(conn, module, matching_zones):
 
     private_zone = bool(vpc_id and vpc_region)
 
-    if zone_in in matching_zones.values():
+    if zone_in in [z['Name'] for z in matching_zones]:
         if hosted_zone_id:
-            changed, result = delete_hosted_id(conn, hosted_zone_id, matching_zones)
+            changed, result = delete_hosted_id(module, client, hosted_zone_id, matching_zones)
         else:
             if private_zone:
-                changed, result = delete_private(conn, matching_zones, vpc_id, vpc_region)
+                changed, result = delete_private(module, client, matching_zones, vpc_id, vpc_region)
             else:
-                changed, result = delete_public(conn, matching_zones)
+                changed, result = delete_public(module, client, matching_zones)
     else:
         changed = False
         result = "No zone to delete."
@@ -308,10 +367,7 @@ def main():
         vpc_region=dict(default=None),
         comment=dict(default=''),
         hosted_zone_id=dict()))
-    module = AnsibleModule(argument_spec=argument_spec)
-
-    if not HAS_BOTO:
-        module.fail_json(msg='boto required for this module')
+    module = AnsibleAWSModule(argument_spec=argument_spec)
 
     zone_in = module.params.get('zone').lower()
     state = module.params.get('state').lower()
@@ -323,24 +379,19 @@ def main():
 
     private_zone = bool(vpc_id and vpc_region)
 
-    _, _, aws_connect_kwargs = get_aws_connection_info(module)
+    client = module.client('route53')
 
-    # connect to the route53 endpoint
-    try:
-        conn = Route53Connection(**aws_connect_kwargs)
-    except boto.exception.BotoServerError as e:
-        module.fail_json(msg=e.error_message)
-
-    zones = find_zones(conn, zone_in, private_zone)
+    zones = find_zones(module, client, zone_in, private_zone)
     if state == 'present':
-        changed, result = create(conn, module, matching_zones=zones)
+        changed, result = create(module, client, matching_zones=zones)
     elif state == 'absent':
-        changed, result = delete(conn, module, matching_zones=zones)
+        changed, result = delete(module, client, matching_zones=zones)
 
     if isinstance(result, dict):
         module.exit_json(changed=changed, result=result, **result)
     else:
         module.exit_json(changed=changed, result=result)
 
+
 if __name__ == '__main__':
     main()


Test patch:
diff --git a/hacking/aws_config/testing_policies/network-policy.json b/hacking/aws_config/testing_policies/network-policy.json
new file mode 100644
--- /dev/null
+++ b/hacking/aws_config/testing_policies/network-policy.json
@@ -0,0 +1,32 @@
+{
+    "Version": "2012-10-17",
+    "Statement": [
+        {
+            "Sid": "ManageVPCsForRoute53Testing",
+            "Effect": "Allow",
+            "Action": [
+                "ec2:CreateTags",
+                "ec2:CreateVpc",
+                "ec2:DeleteVpc",
+                "ec2:DescribeTags",
+                "ec2:DescribeVpcAttribute",
+                "ec2:DescribeVpcClassicLink",
+                "ec2:DescribeVpcs",
+                "ec2:ModifyVpcAttribute"
+            ],
+            "Resource": "*"
+        },
+        {
+            "Sid": "ManageRoute53ForTests",
+            "Effect": "Allow",
+            "Action": [
+                "route53:CreateHostedZone",
+                "route53:DeleteHostedZone",
+                "route53:GetHostedZone",
+                "route53:ListHostedZones",
+                "route53:UpdateHostedZoneComment"
+            ],
+            "Resource": "*"
+        }
+    ]
+}
diff --git a/test/integration/targets/route53_zone/aliases b/test/integration/targets/route53_zone/aliases
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/route53_zone/aliases
@@ -0,0 +1,2 @@
+cloud/aws
+posix/ci/cloud/group4/aws
diff --git a/test/integration/targets/route53_zone/tasks/main.yml b/test/integration/targets/route53_zone/tasks/main.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/route53_zone/tasks/main.yml
@@ -0,0 +1,226 @@
+---
+- block:
+
+    # ============================================================
+    - name: set connection information for all tasks
+      set_fact:
+        aws_connection_info: &aws_connection_info
+          aws_access_key: "{{ aws_access_key }}"
+          aws_secret_key: "{{ aws_secret_key }}"
+          security_token: "{{ security_token }}"
+          region: "{{ aws_region }}"
+      no_log: true
+
+    - name: Create VPC for use in testing
+      ec2_vpc_net:
+        name: "{{ resource_prefix }}-vpc"
+        cidr_block: 10.22.32.0/23
+        tags:
+          Name: Ansible ec2_instance Testing VPC
+        tenancy: default
+        <<: *aws_connection_info
+      register: testing_vpc
+
+    # ============================================================
+    - name: Create a public zone
+      route53_zone:
+        zone: "{{ resource_prefix }}.public"
+        comment: original comment
+        state: present
+        <<: *aws_connection_info
+      register: output
+
+    - assert:
+        that:
+          - output.changed
+          - output.comment == 'original comment'
+          - output.name == '{{ resource_prefix }}.public.'
+          - not output.private_zone
+
+    # ============================================================
+    - name: Do an idemptotent update of a public zone
+      route53_zone:
+        zone: "{{ resource_prefix }}.public"
+        comment: original comment
+        state: present
+        <<: *aws_connection_info
+      register: output
+
+    - assert:
+        that:
+          - not output.changed
+          - output.comment == 'original comment'
+          - output.name == '{{ resource_prefix }}.public.'
+          - not output.private_zone
+
+    # ============================================================
+    - name: Update comment of a public zone
+      route53_zone:
+        zone: "{{ resource_prefix }}.public"
+        comment: updated comment
+        state: present
+        <<: *aws_connection_info
+      register: output
+
+    - assert:
+        that:
+          - output.changed
+          - output.result.comment == "updated comment"
+
+    # ============================================================
+    - name: Delete public zone
+      route53_zone:
+        zone: "{{ resource_prefix }}.public"
+        state: absent
+        <<: *aws_connection_info
+      register: output
+    - assert:
+        that:
+          - output.changed
+          - "'Successfully deleted' in output.result"
+
+    # ============================================================
+    - name: Create a private zone
+      route53_zone:
+        vpc_id: "{{ testing_vpc.vpc.id }}"
+        vpc_region: "{{ aws_region }}"
+        zone: "{{ resource_prefix }}.private"
+        comment: original comment
+        state: present
+        <<: *aws_connection_info
+
+
+    # ============================================================
+    - name: Idemptotent update a private zone
+      route53_zone:
+        vpc_id: "{{ testing_vpc.vpc.id }}"
+        vpc_region: "{{ aws_region }}"
+        zone: "{{ resource_prefix }}.private"
+        comment: original comment
+        state: present
+        <<: *aws_connection_info
+      register: output
+
+    - assert:
+        that:
+          - not output.changed
+          - "'There is already a private hosted zone in the same region with the same VPC' in output.msg"
+
+    # ============================================================
+    - name: Update private zone comment
+      route53_zone:
+        vpc_id: "{{ testing_vpc.vpc.id }}"
+        vpc_region: "{{ aws_region }}"
+        zone: "{{ resource_prefix }}.private"
+        comment: updated_comment
+        state: present
+        <<: *aws_connection_info
+      register: output
+
+    - assert:
+        that:
+          - output.changed
+          - output.result.comment == "updated_comment"
+
+    # ============================================================
+    - name: Try to delete private zone without setting vpc_id and vpc_region
+      route53_zone:
+        zone: "{{ resource_prefix }}.private"
+        state: absent
+        <<: *aws_connection_info
+      register: output
+
+    - assert:
+        that:
+          - not output.changed
+          - "output.result == 'No zone to delete.'"
+
+    # ============================================================
+    - name: Try to delete a public zone that does not exists
+      route53_zone:
+        zone: "{{ resource_prefix }}.publicfake"
+        comment: original comment
+        state: absent
+        <<: *aws_connection_info
+      register: output
+
+    - assert:
+        that:
+          - not output.changed
+          - "output.result == 'No zone to delete.'"
+
+    # ============================================================
+    - name: Delete private zone
+      route53_zone:
+        vpc_id: "{{ testing_vpc.vpc.id }}"
+        vpc_region: "{{ aws_region }}"
+        zone: "{{ resource_prefix }}.private"
+        state: absent
+        <<: *aws_connection_info
+      register: output
+
+    - assert:
+        that:
+          - output.changed
+          - "'Successfully deleted' in output.result"
+
+    # ============================================================
+    - name: Create a public zone
+      route53_zone:
+        zone: "{{ resource_prefix }}.public2"
+        comment: this is an example
+        state: present
+        <<: *aws_connection_info
+      register: new_zone
+
+    # Delete zone using its id
+    - name: Delete zone using attribute hosted_zone_id
+      route53_zone:
+        zone: "{{ resource_prefix }}.public2"
+        hosted_zone_id: "{{new_zone.zone_id}}"
+        state: absent
+        <<: *aws_connection_info
+      register: output
+
+    - assert:
+        that:
+          - output.changed
+          - "'Successfully deleted' in output.result"
+
+  # ============================================================
+  always:
+    - name: Ensure public zone is deleted
+      route53_zone:
+        zone: "{{ item }}"
+        state: absent
+        <<: *aws_connection_info
+      register: removed
+      until: removed is not failed
+      ignore_errors: yes
+      retries: 10
+      with_items:
+        - "{{ resource_prefix }}.public"
+        - "{{ resource_prefix }}.public2"
+
+    - name: Ensure private zone is deleted
+      route53_zone:
+        vpc_id: "{{ testing_vpc.vpc.id }}"
+        vpc_region: "{{ aws_region }}"
+        zone: "{{ resource_prefix }}.private"
+        state: absent
+        <<: *aws_connection_info
+      register: removed
+      until: removed is not failed
+      ignore_errors: yes
+      retries: 10
+
+    - name: remove the VPC
+      ec2_vpc_net:
+        name: "{{ resource_prefix }}-vpc"
+        cidr_block: 10.22.32.0/23
+        state: absent
+        <<: *aws_connection_info
+      register: removed
+      until: removed is not failed
+      ignore_errors: yes
+      retries: 10