1---
2- name: set up aws connection info
3  set_fact:
4    aws_connection_info: &aws_connection_info
5      aws_access_key: "{{ aws_access_key | default(omit) }}"
6      aws_secret_key: "{{ aws_secret_key | default(omit) }}"
7      security_token: "{{ security_token | default(omit) }}"
8      region: "{{ aws_region | default(omit) }}"
9  no_log: yes
10
11- module_defaults:
12    cloudformation:
13      <<: *aws_connection_info
14    cloudformation_info:
15      <<: *aws_connection_info
16
17  block:
18
19    # ==== Env setup ==========================================================
20    - name: list available AZs
21      aws_az_info:
22        <<: *aws_connection_info
23      register: region_azs
24
25    - name: pick an AZ for testing
26      set_fact:
27        availability_zone: "{{ region_azs.availability_zones[0].zone_name }}"
28
29    - name: Create a test VPC
30      ec2_vpc_net:
31        name: "{{ vpc_name }}"
32        cidr_block: "{{ vpc_cidr }}"
33        tags:
34          Name: Cloudformation testing
35        <<: *aws_connection_info
36      register: testing_vpc
37
38    - name: Create a test subnet
39      ec2_vpc_subnet:
40        vpc_id: "{{ testing_vpc.vpc.id }}"
41        cidr: "{{ subnet_cidr }}"
42        az: "{{ availability_zone }}"
43        <<: *aws_connection_info
44      register: testing_subnet
45
46    - name: Find AMI to use
47      ec2_ami_info:
48        owners: 'amazon'
49        filters:
50          name: '{{ ec2_ami_name }}'
51        <<: *aws_connection_info
52      register: ec2_amis
53
54    - name: Set fact with latest AMI
55      vars:
56        latest_ami: '{{ ec2_amis.images | sort(attribute="creation_date") | last }}'
57      set_fact:
58        ec2_ami_image: '{{ latest_ami.image_id }}'
59
60    # ==== Cloudformation tests ===============================================
61
62    # 1. Basic stack creation (check mode, actual run and idempotency)
63    # 2. Tags
64    # 3. cloudformation_info tests (basic + all_facts)
65    # 4. termination_protection
66    # 5. create_changeset + changeset_name
67
68    # There is still scope to add tests for -
69    # 1. capabilities
70    # 2. stack_policy
71    # 3. on_create_failure (covered in unit tests)
72    # 4. Passing in a role
73    # 5. nested stacks?
74
75
76    - name: create a cloudformation stack (check mode)
77      cloudformation:
78        stack_name: "{{ stack_name }}"
79        template_body: "{{ lookup('file','cf_template.json') }}"
80        template_parameters:
81          InstanceType: "t3.nano"
82          ImageId: "{{ ec2_ami_image }}"
83          SubnetId: "{{ testing_subnet.subnet.id }}"
84        tags:
85          Stack: "{{ stack_name }}"
86          test: "{{ resource_prefix }}"
87      register: cf_stack
88      check_mode: yes
89
90    - name: check task return attributes
91      assert:
92        that:
93          - cf_stack.changed
94          - "'msg' in cf_stack and 'New stack would be created' in cf_stack.msg"
95
96    - name: create a cloudformation stack
97      cloudformation:
98        stack_name: "{{ stack_name }}"
99        template_body: "{{ lookup('file','cf_template.json') }}"
100        template_parameters:
101          InstanceType: "t3.nano"
102          ImageId: "{{ ec2_ami_image }}"
103          SubnetId: "{{ testing_subnet.subnet.id }}"
104        tags:
105          Stack: "{{ stack_name }}"
106          test: "{{ resource_prefix }}"
107      register: cf_stack
108
109    - name: check task return attributes
110      assert:
111        that:
112          - cf_stack.changed
113          - "'events' in cf_stack"
114          - "'output' in cf_stack and 'Stack CREATE complete' in cf_stack.output"
115          - "'stack_outputs' in cf_stack and 'InstanceId' in cf_stack.stack_outputs"
116          - "'stack_resources' in cf_stack"
117
118    - name: create a cloudformation stack (check mode) (idempotent)
119      cloudformation:
120        stack_name: "{{ stack_name }}"
121        template_body: "{{ lookup('file','cf_template.json') }}"
122        template_parameters:
123          InstanceType: "t3.nano"
124          ImageId: "{{ ec2_ami_image }}"
125          SubnetId: "{{ testing_subnet.subnet.id }}"
126        tags:
127          Stack: "{{ stack_name }}"
128          test: "{{ resource_prefix }}"
129      register: cf_stack
130      check_mode: yes
131
132    - name: check task return attributes
133      assert:
134        that:
135          - not cf_stack.changed
136
137    - name: create a cloudformation stack (idempotent)
138      cloudformation:
139        stack_name: "{{ stack_name }}"
140        template_body: "{{ lookup('file','cf_template.json') }}"
141        template_parameters:
142          InstanceType: "t3.nano"
143          ImageId: "{{ ec2_ami_image }}"
144          SubnetId: "{{ testing_subnet.subnet.id }}"
145        tags:
146          Stack: "{{ stack_name }}"
147          test: "{{ resource_prefix }}"
148      register: cf_stack
149
150    - name: check task return attributes
151      assert:
152        that:
153          - not cf_stack.changed
154          - "'output' in cf_stack and 'Stack is already up-to-date.' in cf_stack.output"
155          - "'stack_outputs' in cf_stack and 'InstanceId' in cf_stack.stack_outputs"
156          - "'stack_resources' in cf_stack"
157
158    - name: get stack details
159      cloudformation_info:
160        stack_name: "{{ stack_name }}"
161      register: stack_info
162
163    - name: assert stack info
164      assert:
165        that:
166          - "'cloudformation' in stack_info"
167          - "stack_info.cloudformation | length == 1"
168          - "stack_name in stack_info.cloudformation"
169          - "'stack_description' in stack_info.cloudformation[stack_name]"
170          - "'stack_outputs' in stack_info.cloudformation[stack_name]"
171          - "'stack_parameters' in stack_info.cloudformation[stack_name]"
172          - "'stack_tags' in stack_info.cloudformation[stack_name]"
173          - "stack_info.cloudformation[stack_name].stack_tags.Stack == stack_name"
174
175    - name: get stack details (checkmode)
176      cloudformation_info:
177        stack_name: "{{ stack_name }}"
178      register: stack_info
179      check_mode: yes
180
181    - name: assert stack info
182      assert:
183        that:
184          - "'cloudformation' in stack_info"
185          - "stack_info.cloudformation | length == 1"
186          - "stack_name in stack_info.cloudformation"
187          - "'stack_description' in stack_info.cloudformation[stack_name]"
188          - "'stack_outputs' in stack_info.cloudformation[stack_name]"
189          - "'stack_parameters' in stack_info.cloudformation[stack_name]"
190          - "'stack_tags' in stack_info.cloudformation[stack_name]"
191          - "stack_info.cloudformation[stack_name].stack_tags.Stack == stack_name"
192
193    - name: get stack details (all_facts)
194      cloudformation_info:
195        stack_name: "{{ stack_name }}"
196        all_facts: yes
197      register: stack_info
198
199    - name: assert stack info
200      assert:
201        that:
202          - "'stack_events' in stack_info.cloudformation[stack_name]"
203          - "'stack_policy' in stack_info.cloudformation[stack_name]"
204          - "'stack_resource_list' in stack_info.cloudformation[stack_name]"
205          - "'stack_resources' in stack_info.cloudformation[stack_name]"
206          - "'stack_template' in stack_info.cloudformation[stack_name]"
207
208    - name: get stack details (all_facts) (checkmode)
209      cloudformation_info:
210        stack_name: "{{ stack_name }}"
211        all_facts: yes
212      register: stack_info
213      check_mode: yes
214
215    - name: assert stack info
216      assert:
217        that:
218          - "'stack_events' in stack_info.cloudformation[stack_name]"
219          - "'stack_policy' in stack_info.cloudformation[stack_name]"
220          - "'stack_resource_list' in stack_info.cloudformation[stack_name]"
221          - "'stack_resources' in stack_info.cloudformation[stack_name]"
222          - "'stack_template' in stack_info.cloudformation[stack_name]"
223
224    # ==== Cloudformation tests (create changeset) ============================
225
226    # try to create a changeset by changing instance type
227    - name: create a changeset
228      cloudformation:
229        stack_name: "{{ stack_name }}"
230        create_changeset: yes
231        changeset_name: "test-changeset"
232        template_body: "{{ lookup('file','cf_template.json') }}"
233        template_parameters:
234          InstanceType: "t3.micro"
235          ImageId: "{{ ec2_ami_image }}"
236          SubnetId: "{{ testing_subnet.subnet.id }}"
237        tags:
238          Stack: "{{ stack_name }}"
239          test: "{{ resource_prefix }}"
240      register: create_changeset_result
241
242    - name: assert changeset created
243      assert:
244        that:
245          - "create_changeset_result.changed"
246          - "'change_set_id' in create_changeset_result"
247          - "'Stack CREATE_CHANGESET complete' in create_changeset_result.output"
248
249    - name: get stack details with changesets
250      cloudformation_info:
251        stack_name: "{{ stack_name }}"
252        stack_change_sets: True
253      register: stack_info
254
255    - name: assert changesets in info
256      assert:
257        that:
258          - "'stack_change_sets' in stack_info.cloudformation[stack_name]"
259
260    - name: get stack details with changesets (checkmode)
261      cloudformation_info:
262        stack_name: "{{ stack_name }}"
263        stack_change_sets: True
264      register: stack_info
265      check_mode: yes
266
267    - name: assert changesets in info
268      assert:
269        that:
270          - "'stack_change_sets' in stack_info.cloudformation[stack_name]"
271
272    # try to create an empty changeset by passing in unchanged template
273    - name: create a changeset
274      cloudformation:
275        stack_name: "{{ stack_name }}"
276        create_changeset: yes
277        template_body: "{{ lookup('file','cf_template.json') }}"
278        template_parameters:
279          InstanceType: "t3.nano"
280          ImageId: "{{ ec2_ami_image }}"
281          SubnetId: "{{ testing_subnet.subnet.id }}"
282        tags:
283          Stack: "{{ stack_name }}"
284          test: "{{ resource_prefix }}"
285      register: create_changeset_result
286
287    - name: assert changeset created
288      assert:
289        that:
290          - "not create_changeset_result.changed"
291          - "'The created Change Set did not contain any changes to this stack and was deleted.' in create_changeset_result.output"
292
293    # ==== Cloudformation tests (termination_protection) ======================
294
295    - name: set termination protection to true
296      cloudformation:
297        stack_name: "{{ stack_name }}"
298        termination_protection: yes
299        template_body: "{{ lookup('file','cf_template.json') }}"
300        template_parameters:
301          InstanceType: "t3.nano"
302          ImageId: "{{ ec2_ami_image }}"
303          SubnetId: "{{ testing_subnet.subnet.id }}"
304        tags:
305          Stack: "{{ stack_name }}"
306          test: "{{ resource_prefix }}"
307      register: cf_stack
308
309#    This fails - #65592
310#    - name: check task return attributes
311#      assert:
312#        that:
313#          - cf_stack.changed
314
315    - name: get stack details
316      cloudformation_info:
317        stack_name: "{{ stack_name }}"
318      register: stack_info
319
320    - name: assert stack info
321      assert:
322        that:
323          - "stack_info.cloudformation[stack_name].stack_description.enable_termination_protection"
324
325    - name: get stack details (checkmode)
326      cloudformation_info:
327        stack_name: "{{ stack_name }}"
328      register: stack_info
329      check_mode: yes
330
331    - name: assert stack info
332      assert:
333        that:
334          - "stack_info.cloudformation[stack_name].stack_description.enable_termination_protection"
335
336    - name: set termination protection to false
337      cloudformation:
338        stack_name: "{{ stack_name }}"
339        termination_protection: no
340        template_body: "{{ lookup('file','cf_template.json') }}"
341        template_parameters:
342          InstanceType: "t3.nano"
343          ImageId: "{{ ec2_ami_image }}"
344          SubnetId: "{{ testing_subnet.subnet.id }}"
345        tags:
346          Stack: "{{ stack_name }}"
347          test: "{{ resource_prefix }}"
348      register: cf_stack
349
350#    This fails - #65592
351#    - name: check task return attributes
352#      assert:
353#        that:
354#          - cf_stack.changed
355
356    - name: get stack details
357      cloudformation_info:
358        stack_name: "{{ stack_name }}"
359      register: stack_info
360
361    - name: assert stack info
362      assert:
363        that:
364          - "not stack_info.cloudformation[stack_name].stack_description.enable_termination_protection"
365
366    - name: get stack details (checkmode)
367      cloudformation_info:
368        stack_name: "{{ stack_name }}"
369      register: stack_info
370      check_mode: yes
371
372    - name: assert stack info
373      assert:
374        that:
375          - "not stack_info.cloudformation[stack_name].stack_description.enable_termination_protection"
376
377    # ==== Cloudformation tests (delete stack tests) ==========================
378
379    - name: delete cloudformation stack (check mode)
380      cloudformation:
381        stack_name: "{{ stack_name }}"
382        state: absent
383      check_mode: yes
384      register: cf_stack
385
386    - name: check task return attributes
387      assert:
388        that:
389          - cf_stack.changed
390          - "'msg' in cf_stack and 'Stack would be deleted' in cf_stack.msg"
391
392    - name: delete cloudformation stack
393      cloudformation:
394        stack_name: "{{ stack_name }}"
395        state: absent
396      register: cf_stack
397
398    - name: check task return attributes
399      assert:
400        that:
401          - cf_stack.changed
402          - "'output' in cf_stack and 'Stack Deleted' in cf_stack.output"
403
404    - name: delete cloudformation stack (check mode) (idempotent)
405      cloudformation:
406        stack_name: "{{ stack_name }}"
407        state: absent
408      check_mode: yes
409      register: cf_stack
410
411    - name: check task return attributes
412      assert:
413        that:
414          - not cf_stack.changed
415          - "'msg' in cf_stack"
416          - >-
417            "Stack doesn't exist" in cf_stack.msg
418
419    - name: delete cloudformation stack (idempotent)
420      cloudformation:
421        stack_name: "{{ stack_name }}"
422        state: absent
423      register: cf_stack
424
425    - name: check task return attributes
426      assert:
427        that:
428          - not cf_stack.changed
429          - "'output' in cf_stack and 'Stack not found.' in cf_stack.output"
430
431    - name: get stack details
432      cloudformation_info:
433        stack_name: "{{ stack_name }}"
434      register: stack_info
435
436    - name: assert stack info
437      assert:
438        that:
439          - "not stack_info.cloudformation"
440
441    - name: get stack details (checkmode)
442      cloudformation_info:
443        stack_name: "{{ stack_name }}"
444      register: stack_info
445      check_mode: yes
446
447    - name: assert stack info
448      assert:
449        that:
450          - "not stack_info.cloudformation"
451
452    # ==== Cleanup ============================================================
453
454  always:
455
456    - name: delete stack
457      cloudformation:
458        stack_name: "{{ stack_name }}"
459        state: absent
460      ignore_errors: yes
461
462    - name: Delete test subnet
463      ec2_vpc_subnet:
464        vpc_id: "{{ testing_vpc.vpc.id }}"
465        cidr: "{{ subnet_cidr }}"
466        state: absent
467        <<: *aws_connection_info
468      ignore_errors: yes
469
470    - name: Delete test VPC
471      ec2_vpc_net:
472        name: "{{ vpc_name }}"
473        cidr_block: "{{ vpc_cidr }}"
474        state: absent
475        <<: *aws_connection_info
476      ignore_errors: yes
477