--- - name: set up aws connection info set_fact: aws_connection_info: &aws_connection_info aws_access_key: "{{ aws_access_key | default(omit) }}" aws_secret_key: "{{ aws_secret_key | default(omit) }}" security_token: "{{ security_token | default(omit) }}" region: "{{ aws_region | default(omit) }}" no_log: yes - module_defaults: cloudformation: <<: *aws_connection_info cloudformation_info: <<: *aws_connection_info block: # ==== Env setup ========================================================== - name: list available AZs aws_az_info: <<: *aws_connection_info register: region_azs - name: pick an AZ for testing set_fact: availability_zone: "{{ region_azs.availability_zones[0].zone_name }}" - name: Create a test VPC ec2_vpc_net: name: "{{ vpc_name }}" cidr_block: "{{ vpc_cidr }}" tags: Name: Cloudformation testing <<: *aws_connection_info register: testing_vpc - name: Create a test subnet ec2_vpc_subnet: vpc_id: "{{ testing_vpc.vpc.id }}" cidr: "{{ subnet_cidr }}" az: "{{ availability_zone }}" <<: *aws_connection_info register: testing_subnet - name: Find AMI to use ec2_ami_info: owners: 'amazon' filters: name: '{{ ec2_ami_name }}' <<: *aws_connection_info register: ec2_amis - name: Set fact with latest AMI vars: latest_ami: '{{ ec2_amis.images | sort(attribute="creation_date") | last }}' set_fact: ec2_ami_image: '{{ latest_ami.image_id }}' # ==== Cloudformation tests =============================================== # 1. Basic stack creation (check mode, actual run and idempotency) # 2. Tags # 3. cloudformation_info tests (basic + all_facts) # 4. termination_protection # 5. create_changeset + changeset_name # There is still scope to add tests for - # 1. capabilities # 2. stack_policy # 3. on_create_failure (covered in unit tests) # 4. Passing in a role # 5. nested stacks? - name: create a cloudformation stack (check mode) cloudformation: stack_name: "{{ stack_name }}" template_body: "{{ lookup('file','cf_template.json') }}" template_parameters: InstanceType: "t3.nano" ImageId: "{{ ec2_ami_image }}" SubnetId: "{{ testing_subnet.subnet.id }}" tags: Stack: "{{ stack_name }}" test: "{{ resource_prefix }}" register: cf_stack check_mode: yes - name: check task return attributes assert: that: - cf_stack.changed - "'msg' in cf_stack and 'New stack would be created' in cf_stack.msg" - name: create a cloudformation stack cloudformation: stack_name: "{{ stack_name }}" template_body: "{{ lookup('file','cf_template.json') }}" template_parameters: InstanceType: "t3.nano" ImageId: "{{ ec2_ami_image }}" SubnetId: "{{ testing_subnet.subnet.id }}" tags: Stack: "{{ stack_name }}" test: "{{ resource_prefix }}" register: cf_stack - name: check task return attributes assert: that: - cf_stack.changed - "'events' in cf_stack" - "'output' in cf_stack and 'Stack CREATE complete' in cf_stack.output" - "'stack_outputs' in cf_stack and 'InstanceId' in cf_stack.stack_outputs" - "'stack_resources' in cf_stack" - name: create a cloudformation stack (check mode) (idempotent) cloudformation: stack_name: "{{ stack_name }}" template_body: "{{ lookup('file','cf_template.json') }}" template_parameters: InstanceType: "t3.nano" ImageId: "{{ ec2_ami_image }}" SubnetId: "{{ testing_subnet.subnet.id }}" tags: Stack: "{{ stack_name }}" test: "{{ resource_prefix }}" register: cf_stack check_mode: yes - name: check task return attributes assert: that: - not cf_stack.changed - name: create a cloudformation stack (idempotent) cloudformation: stack_name: "{{ stack_name }}" template_body: "{{ lookup('file','cf_template.json') }}" template_parameters: InstanceType: "t3.nano" ImageId: "{{ ec2_ami_image }}" SubnetId: "{{ testing_subnet.subnet.id }}" tags: Stack: "{{ stack_name }}" test: "{{ resource_prefix }}" register: cf_stack - name: check task return attributes assert: that: - not cf_stack.changed - "'output' in cf_stack and 'Stack is already up-to-date.' in cf_stack.output" - "'stack_outputs' in cf_stack and 'InstanceId' in cf_stack.stack_outputs" - "'stack_resources' in cf_stack" - name: get stack details cloudformation_info: stack_name: "{{ stack_name }}" register: stack_info - name: assert stack info assert: that: - "'cloudformation' in stack_info" - "stack_info.cloudformation | length == 1" - "stack_name in stack_info.cloudformation" - "'stack_description' in stack_info.cloudformation[stack_name]" - "'stack_outputs' in stack_info.cloudformation[stack_name]" - "'stack_parameters' in stack_info.cloudformation[stack_name]" - "'stack_tags' in stack_info.cloudformation[stack_name]" - "stack_info.cloudformation[stack_name].stack_tags.Stack == stack_name" - name: get stack details (checkmode) cloudformation_info: stack_name: "{{ stack_name }}" register: stack_info check_mode: yes - name: assert stack info assert: that: - "'cloudformation' in stack_info" - "stack_info.cloudformation | length == 1" - "stack_name in stack_info.cloudformation" - "'stack_description' in stack_info.cloudformation[stack_name]" - "'stack_outputs' in stack_info.cloudformation[stack_name]" - "'stack_parameters' in stack_info.cloudformation[stack_name]" - "'stack_tags' in stack_info.cloudformation[stack_name]" - "stack_info.cloudformation[stack_name].stack_tags.Stack == stack_name" - name: get stack details (all_facts) cloudformation_info: stack_name: "{{ stack_name }}" all_facts: yes register: stack_info - name: assert stack info assert: that: - "'stack_events' in stack_info.cloudformation[stack_name]" - "'stack_policy' in stack_info.cloudformation[stack_name]" - "'stack_resource_list' in stack_info.cloudformation[stack_name]" - "'stack_resources' in stack_info.cloudformation[stack_name]" - "'stack_template' in stack_info.cloudformation[stack_name]" - name: get stack details (all_facts) (checkmode) cloudformation_info: stack_name: "{{ stack_name }}" all_facts: yes register: stack_info check_mode: yes - name: assert stack info assert: that: - "'stack_events' in stack_info.cloudformation[stack_name]" - "'stack_policy' in stack_info.cloudformation[stack_name]" - "'stack_resource_list' in stack_info.cloudformation[stack_name]" - "'stack_resources' in stack_info.cloudformation[stack_name]" - "'stack_template' in stack_info.cloudformation[stack_name]" # ==== Cloudformation tests (create changeset) ============================ # try to create a changeset by changing instance type - name: create a changeset cloudformation: stack_name: "{{ stack_name }}" create_changeset: yes changeset_name: "test-changeset" template_body: "{{ lookup('file','cf_template.json') }}" template_parameters: InstanceType: "t3.micro" ImageId: "{{ ec2_ami_image }}" SubnetId: "{{ testing_subnet.subnet.id }}" tags: Stack: "{{ stack_name }}" test: "{{ resource_prefix }}" register: create_changeset_result - name: assert changeset created assert: that: - "create_changeset_result.changed" - "'change_set_id' in create_changeset_result" - "'Stack CREATE_CHANGESET complete' in create_changeset_result.output" - name: get stack details with changesets cloudformation_info: stack_name: "{{ stack_name }}" stack_change_sets: True register: stack_info - name: assert changesets in info assert: that: - "'stack_change_sets' in stack_info.cloudformation[stack_name]" - name: get stack details with changesets (checkmode) cloudformation_info: stack_name: "{{ stack_name }}" stack_change_sets: True register: stack_info check_mode: yes - name: assert changesets in info assert: that: - "'stack_change_sets' in stack_info.cloudformation[stack_name]" # try to create an empty changeset by passing in unchanged template - name: create a changeset cloudformation: stack_name: "{{ stack_name }}" create_changeset: yes template_body: "{{ lookup('file','cf_template.json') }}" template_parameters: InstanceType: "t3.nano" ImageId: "{{ ec2_ami_image }}" SubnetId: "{{ testing_subnet.subnet.id }}" tags: Stack: "{{ stack_name }}" test: "{{ resource_prefix }}" register: create_changeset_result - name: assert changeset created assert: that: - "not create_changeset_result.changed" - "'The created Change Set did not contain any changes to this stack and was deleted.' in create_changeset_result.output" # ==== Cloudformation tests (termination_protection) ====================== - name: set termination protection to true cloudformation: stack_name: "{{ stack_name }}" termination_protection: yes template_body: "{{ lookup('file','cf_template.json') }}" template_parameters: InstanceType: "t3.nano" ImageId: "{{ ec2_ami_image }}" SubnetId: "{{ testing_subnet.subnet.id }}" tags: Stack: "{{ stack_name }}" test: "{{ resource_prefix }}" register: cf_stack # This fails - #65592 # - name: check task return attributes # assert: # that: # - cf_stack.changed - name: get stack details cloudformation_info: stack_name: "{{ stack_name }}" register: stack_info - name: assert stack info assert: that: - "stack_info.cloudformation[stack_name].stack_description.enable_termination_protection" - name: get stack details (checkmode) cloudformation_info: stack_name: "{{ stack_name }}" register: stack_info check_mode: yes - name: assert stack info assert: that: - "stack_info.cloudformation[stack_name].stack_description.enable_termination_protection" - name: set termination protection to false cloudformation: stack_name: "{{ stack_name }}" termination_protection: no template_body: "{{ lookup('file','cf_template.json') }}" template_parameters: InstanceType: "t3.nano" ImageId: "{{ ec2_ami_image }}" SubnetId: "{{ testing_subnet.subnet.id }}" tags: Stack: "{{ stack_name }}" test: "{{ resource_prefix }}" register: cf_stack # This fails - #65592 # - name: check task return attributes # assert: # that: # - cf_stack.changed - name: get stack details cloudformation_info: stack_name: "{{ stack_name }}" register: stack_info - name: assert stack info assert: that: - "not stack_info.cloudformation[stack_name].stack_description.enable_termination_protection" - name: get stack details (checkmode) cloudformation_info: stack_name: "{{ stack_name }}" register: stack_info check_mode: yes - name: assert stack info assert: that: - "not stack_info.cloudformation[stack_name].stack_description.enable_termination_protection" # ==== Cloudformation tests (delete stack tests) ========================== - name: delete cloudformation stack (check mode) cloudformation: stack_name: "{{ stack_name }}" state: absent check_mode: yes register: cf_stack - name: check task return attributes assert: that: - cf_stack.changed - "'msg' in cf_stack and 'Stack would be deleted' in cf_stack.msg" - name: delete cloudformation stack cloudformation: stack_name: "{{ stack_name }}" state: absent register: cf_stack - name: check task return attributes assert: that: - cf_stack.changed - "'output' in cf_stack and 'Stack Deleted' in cf_stack.output" - name: delete cloudformation stack (check mode) (idempotent) cloudformation: stack_name: "{{ stack_name }}" state: absent check_mode: yes register: cf_stack - name: check task return attributes assert: that: - not cf_stack.changed - "'msg' in cf_stack" - >- "Stack doesn't exist" in cf_stack.msg - name: delete cloudformation stack (idempotent) cloudformation: stack_name: "{{ stack_name }}" state: absent register: cf_stack - name: check task return attributes assert: that: - not cf_stack.changed - "'output' in cf_stack and 'Stack not found.' in cf_stack.output" - name: get stack details cloudformation_info: stack_name: "{{ stack_name }}" register: stack_info - name: assert stack info assert: that: - "not stack_info.cloudformation" - name: get stack details (checkmode) cloudformation_info: stack_name: "{{ stack_name }}" register: stack_info check_mode: yes - name: assert stack info assert: that: - "not stack_info.cloudformation" # ==== Cleanup ============================================================ always: - name: delete stack cloudformation: stack_name: "{{ stack_name }}" state: absent ignore_errors: yes - name: Delete test subnet ec2_vpc_subnet: vpc_id: "{{ testing_vpc.vpc.id }}" cidr: "{{ subnet_cidr }}" state: absent <<: *aws_connection_info ignore_errors: yes - name: Delete test VPC ec2_vpc_net: name: "{{ vpc_name }}" cidr_block: "{{ vpc_cidr }}" state: absent <<: *aws_connection_info ignore_errors: yes