Repository: ansible/ansible

Issue:
Add Jinja2 templates to Cloudformation module
##### ISSUE TYPE

 - Feature Idea

##### COMPONENT NAME

AWS Cloudformation

##### SUMMARY

Using Jinja2 to produce Cloudformation templates allows for a flexible infra provisioning solution, however at the moment it is necessary to render a Jinja2 CF template into a temp file so it can be used to invoke the CF module like so:

```YAML
---

- name: Create a temp file for CF template
  command: mktemp
  register: tmpfile

- name: Create CF template
  template:
    dest: "{{ tmpfile.stdout }}"
    src: template.json.j2

- name: Ensure CF stack
  cloudformation:
    region: "..."
    stack_name: "..."
    state: present
    template: "{{ tmpfile.stdout }}"

- name: cleanup the temp file
  command: "rm {{ tmpfile.stdout }}"
```

It would be nice if the CF module could be invoked with a Jinja template directly:

```YAML
---

- name: Ensure CF stack
  cloudformation:
    format: j2
    region: "..."
    stack_name: "..."
    state: present
    template: template.json.j2
```


Discussion:
cc @tedder @ryansb
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
or you can use the lookup:

```
template: "{{lookup('template', 'template.json.j2')}}"
```
@bcoca I don't think that will work, the template parameter expects a file name that it will then lookup, I don't think I can supply raw JSON here.
i'm not sure what you mean, but it would work based on the examples above
The template parameter of the Cloudformation module expects a path/filename, not a raw CF template.
a template filter then would make more sense, but i dont understand why not just template 'in situ' if what you are passing is 'raw'. 

in any case, that is not what example shows, it shows taking a file directly
The best way to do this:

- write out a template
- (optionally) upload to s3 (for larger configs)
- use that template in cloudformation
So in my example I create a tmp file, write out a Jinja template to the file and then pass the tmp files name into the module. This works at the moment, but I think adding Jinja rendering into the CF module would be a good addition.
I really don't think so, as 'template' is very complex onto itself and has many thing to deal with, aside from maintaing duplicate code. Using etiher inline templating or the template lookup should suffice, I still don't see a clear case in which it doesn't.
I thought so too at one point, David, but there are some good reasons not to- for instance, if jinja rendering was in CF, it couldn't be moved to S3. Or that could be added, and.. turtles the whole way down.
`wontfix` perhaps the best thing would be to give a canonical example in the 'examples'.
I believe a 'better' fix is to allow the template option (or new option) to take the template as a string instead of path, then you can use 'inline templating' and avoid adding templating code into the module itself.
I don't think this is a wontfix - seems pretty reasonable to accept template as a string since, as we all know, Ansible users ❤️ jinja2 templates, and CFN's string assemblers are sorta 💩 . 
I'd propose adding a `template_body` parameter that will accept raw string input so lookups (such as `template`) will work with this module. 
bot_status

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
@@ -48,8 +48,9 @@
       - The local path of the cloudformation template.
       - This must be the full path to the file, relative to the working directory. If using roles this may look
         like "roles/cloudformation/files/cloudformation-example.json".
-      - If 'state' is 'present' and the stack does not exist yet, either 'template' or 'template_url' must be specified (but not both). If 'state' is
-        present, the stack does exist, and neither 'template' nor 'template_url' are specified, the previous template will be reused.
+      - If 'state' is 'present' and the stack does not exist yet, either 'template', 'template_body' or 'template_url'
+        must be specified (but only one of them). If 'state' ispresent, the stack does exist, and neither 'template',
+        'template_body' nor 'template_url' are specified, the previous template will be reused.
     required: false
     default: null
   notification_arns:
@@ -75,8 +76,9 @@
     description:
       - Location of file containing the template body. The URL must point to a template (max size 307,200 bytes) located in an S3 bucket in the same region
         as the stack.
-      - If 'state' is 'present' and the stack does not exist yet, either 'template' or 'template_url' must be specified (but not both). If 'state' is
-        present, the stack does exist, and neither 'template' nor 'template_url' are specified, the previous template will be reused.
+      - If 'state' is 'present' and the stack does not exist yet, either 'template', 'template_body' or 'template_url'
+        must be specified (but only one of them). If 'state' ispresent, the stack does exist, and neither 'template',
+        'template_body' nor 'template_url' are specified, the previous template will be reused.
     required: false
     version_added: "2.0"
   create_changeset:
@@ -115,6 +117,14 @@
     description:
     - enable or disable termination protection on the stack. Only works with botocore >= 1.7.18.
     version_added: "2.5"
+  template_body:
+    description:
+      - Template body. Use this to pass in the actual body of the Cloudformation template.
+      - If 'state' is 'present' and the stack does not exist yet, either 'template', 'template_body' or 'template_url'
+        must be specified (but only one of them). If 'state' ispresent, the stack does exist, and neither 'template',
+        'template_body' nor 'template_url' are specified, the previous template will be reused.
+    required: false
+    version_added: "2.5"
 
 author: "James S. Martin (@jsmartin)"
 extends_documentation_fragment:
@@ -171,6 +181,23 @@
     tags:
       Stack: ansible-cloudformation
 
+# Create a stack, passing in template body using lookup of Jinja2 template, disable rollback if stack creation fails,
+# pass in some parameters to the template, provide tags for resources created
+- name: create a stack, pass in the template body via lookup template
+  cloudformation:
+    stack_name: "ansible-cloudformation"
+    state: present
+    region: us-east-1
+    disable_rollback: true
+    template_body: "{{ lookup('template', 'cloudformation.j2') }}"
+    template_parameters:
+      KeyName: jmartin
+      DiskType: ephemeral
+      InstanceType: m1.small
+      ClusterSize: 3
+    tags:
+      Stack: ansible-cloudformation
+
 # Enable termination protection on a stack.
 # If the stack already exists, this will update its termination protection
 - name: enable termination protection during stack creation
@@ -272,7 +299,7 @@ def get_stack_events(cfn, stack_name, token_filter=None):
 
 def create_stack(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 when the stack does not exist.")
+        module.fail_json(msg="Either 'template', 'template_body' or 'template_url' is required when the stack does not exist.")
 
     # 'disablerollback' and 'EnableTerminationProtection' only
     # apply on creation, not update.
@@ -497,6 +524,7 @@ def main():
         stack_policy=dict(default=None, required=False),
         disable_rollback=dict(default=False, type='bool'),
         template_url=dict(default=None, required=False),
+        template_body=dict(default=None, require=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),
@@ -508,7 +536,7 @@ def main():
 
     module = AnsibleModule(
         argument_spec=argument_spec,
-        mutually_exclusive=[['template_url', 'template']],
+        mutually_exclusive=[['template_url', 'template', 'template_body']],
         supports_check_mode=True
     )
     if not HAS_BOTO3:
@@ -524,6 +552,8 @@ def main():
 
     if module.params['template'] is not None:
         stack_params['TemplateBody'] = open(module.params['template'], 'r').read()
+    elif module.params['template_body'] is not None:
+        stack_params['TemplateBody'] = module.params['template_body']
     elif module.params['template_url'] is not None:
         stack_params['TemplateURL'] = module.params['template_url']
 


Test patch:
diff --git a/test/units/modules/cloud/amazon/test_cloudformation.py b/test/units/modules/cloud/amazon/test_cloudformation.py
--- a/test/units/modules/cloud/amazon/test_cloudformation.py
+++ b/test/units/modules/cloud/amazon/test_cloudformation.py
@@ -128,4 +128,4 @@ def test_missing_template_body(placeboify):
         )
     assert exc_info.match('FAIL')
     assert not m.exit_args
-    assert "Either 'template' or 'template_url' is required when the stack does not exist." == m.exit_kwargs['msg']
+    assert "Either 'template', 'template_body' or 'template_url' is required when the stack does not exist." == m.exit_kwargs['msg']