Repository: ansible/ansible

Issue:
[Feature request] Add support for Change Sets for Cloud Formation
##### ISSUE TYPE
 - Feature Idea


##### COMPONENT NAME
- CloudFormation

##### ANSIBLE VERSION

```
ansible-playbook 2.3.0.0
  config file = /Users/jaguin/.ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.10 (default, Feb  6 2017, 23:53:20) [GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)]
```

##### CONFIGURATION
N/A

##### OS / ENVIRONMENT
N/A

##### SUMMARY

AWS Cloud formation has "now" support for Change Sets in stacks: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html

It would be nice to have support for this from Ansible as a module or as new parameters to the already existing cloud formation module.

The previous repo contained already a request for this, but it seems it was left as Open (https://github.com/ansible/ansible-modules-core/issues/5625).
##### STEPS TO REPRODUCE
N/A

##### EXPECTED RESULTS
N/A

##### ACTUAL RESULTS
N/A




Discussion:
cc @tedder @ryansb
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I like this. I'll try to test soon.
@tedder is this something that would be considered for 2.3.x?

Resolving patch:
diff --git a/lib/ansible/modules/cloud/amazon/cloudformation.py b/lib/ansible/modules/cloud/amazon/cloudformation.py
--- a/lib/ansible/modules/cloud/amazon/cloudformation.py
+++ b/lib/ansible/modules/cloud/amazon/cloudformation.py
@@ -93,6 +93,23 @@
         present, the stack does exist, and neither 'template' nor 'template_url' are specified, the previous template will be reused.
     required: false
     version_added: "2.0"
+  create_changeset:
+    description:
+      - "If stack already exists create a changeset instead of directly applying changes.
+        See the AWS Change Sets docs U(http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html).
+        WARNING: if the stack does not exist, it will be created without changeset. If the state is absent, the stack will be deleted immediately with no
+        changeset."
+    required: false
+    default: false
+    version_added: "2.4"
+  changeset_name:
+    description:
+      - Name given to the changeset when creating a changeset, only used when create_changeset is true. By default a name prefixed with Ansible-STACKNAME
+        is generated based on input parameters.
+        See the AWS Change Sets docs U(http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html)
+    required: false
+    default: null
+    version_added: "2.4"
   template_format:
     description:
     - (deprecated) For local templates, allows specification of json or yaml format. Templates are now passed raw to CloudFormation regardless of format.
@@ -239,6 +256,7 @@
 # import a class, otherwise we'll use a fully qualified path
 from ansible.module_utils.ec2 import AWSRetry
 from ansible.module_utils.basic import AnsibleModule
+from ansible.utils.hashing import secure_hash_s
 import ansible.module_utils.ec2
 
 def boto_exception(err):
@@ -297,6 +315,45 @@ def create_stack(module, stack_params, cfn):
     return result
 
 
+def list_changesets(cfn, stack_name):
+    res = cfn.list_change_sets(StackName=stack_name)
+    changesets = []
+    for cs in res['Summaries']:
+        changesets.append(cs['ChangeSetName'])
+    return changesets
+
+def create_changeset(module, stack_params, cfn):
+    if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params:
+        module.fail_json(msg="Either 'template' or 'template_url' is required.")
+
+    try:
+        if not 'ChangeSetName' in stack_params:
+            # Determine ChangeSetName using hash of parameters.
+            changeset_name = 'Ansible-' + stack_params['StackName'] + '-' + secure_hash_s(json.dumps(stack_params, sort_keys=True))
+            stack_params['ChangeSetName'] = changeset_name
+        # Determine if this changeset already exists
+        pending_changesets = list_changesets(cfn, stack_params['StackName'])
+        if changeset_name in pending_changesets:
+            warning = 'WARNING: '+str(len(pending_changesets))+' pending changeset(s) exist(s) for this stack!'
+            result = dict(changed=False, output='ChangeSet ' + changeset_name + ' already exists.', warnings=[warning])
+        else:
+            cs = cfn.create_change_set(**stack_params)
+            result = stack_operation(cfn, stack_params['StackName'], 'UPDATE')
+            result['warnings'] = [('Created changeset named ' + changeset_name + ' for stack ' + stack_params['StackName']),
+                ('You can execute it using: aws cloudformation execute-change-set --change-set-name ' + cs['Id']),
+                ('NOTE that dependencies on this stack might fail due to pending changes!')]
+    except Exception as err:
+        error_msg = boto_exception(err)
+        if 'No updates are to be performed.' in error_msg:
+            result = dict(changed=False, output='Stack is already up-to-date.')
+        else:
+            module.fail_json(msg=error_msg)
+
+    if not result:
+        module.fail_json(msg="empty result")
+    return result
+
+
 def update_stack(module, stack_params, cfn):
     if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params:
         stack_params['UsePreviousTemplate'] = True
@@ -400,6 +457,8 @@ def main():
         disable_rollback=dict(default=False, type='bool'),
         template_url=dict(default=None, required=False),
         template_format=dict(default=None, choices=['json', 'yaml'], required=False),
+        create_changeset=dict(default=False, type='bool'),
+        changeset_name=dict(default=None, required=False),
         role_arn=dict(default=None, required=False),
         tags=dict(default=None, type='dict')
     )
@@ -432,6 +491,9 @@ def main():
     if module.params['stack_policy'] is not None:
         stack_params['StackPolicyBody'] = open(module.params['stack_policy'], 'r').read()
 
+    if module.params['changeset_name'] is not None:
+        stack_params['ChangeSetName'] = module.params['changeset_name']
+
     template_parameters = module.params['template_parameters']
     stack_params['Parameters'] = [{'ParameterKey':k, 'ParameterValue':v} for k, v in template_parameters.items()]
 
@@ -454,6 +516,8 @@ def main():
     if state == 'present':
         if not stack_info:
             result = create_stack(module, stack_params, cfn)
+        elif create_changeset:
+            result = create_changeset(module, stack_params, cfn)
         else:
             result = update_stack(module, stack_params, cfn)
 


Test patch: