diff --git a/examples/playbooks/cloudformation.yaml b/examples/playbooks/cloudformation.yaml
new file mode 100644
index 0000000000..242def3a44
--- /dev/null
+++ b/examples/playbooks/cloudformation.yaml
@@ -0,0 +1,45 @@
+---
+# This playbook demonstrates how to use the ansible cloudformation module to launch an AWS CloudFormation stack.
+#
+# This module requires that the boto python library is installed, and that you have your AWS credentials
+# in $HOME/.boto
+
+#The thought here is to bring up a bare infrastructure with CloudFormation, but use ansible to configure it.
+#I generally do this in 2 different playbook runs as to allow the ec2.py inventory to be updated.
+
+#This module also uses "complex arguments" which were introduced in ansible 1.1 allowing you to specify the
+#Cloudformation template parameters
+
+#This example launches a 3 node AutoScale group, with a security group, and an InstanceProfile with root permissions.
+
+#If a stack does not exist, it will be created. If it does exist and the template file has changed, the stack will be updated.
+#If the parameters are different, the stack will also be updated.
+
+#CloudFormation stacks can take awhile to provision, if you are curious about its status, use the AWS
+#web console or one of the CloudFormation CLI's.
+
+#Example update -- try first launching the stack with 3 as the ClusterSize. After it is launched, change it to 4
+#and run the playbook again.
+
+- name: provision stack
+ hosts: localhost
+ connection: local
+ gather_facts: false
+
+ # Launch the cloudformation-example.json template. Register the output.
+
+ tasks:
+ - name: launch ansible cloudformation example
+ cloudformation: >
+ stack_name="ansible-cloudformation" state=present
+ region=us-east-1 disable_rollback=true
+ template=files/cloudformation-example.json
+ args:
+ template_parameters:
+ KeyName: jmartin
+ DiskType: ephemeral
+ InstanceType: m1.small
+ ClusterSize: 3
+ register: stack
+ - name: show stack outputs
+ debug: msg="My stack outputs are ${stack.stack_outputs}"
\ No newline at end of file
diff --git a/examples/playbooks/files/cloudformation-example.json b/examples/playbooks/files/cloudformation-example.json
new file mode 100644
index 0000000000..945e4e4d7c
--- /dev/null
+++ b/examples/playbooks/files/cloudformation-example.json
@@ -0,0 +1,399 @@
+{
+ "Outputs" : {
+ "ClusterSecGroup" : {
+ "Description" : "Name of RegionalManagerSecGroup",
+ "Value" : {
+ "Ref" : "InstanceSecurityGroup"
+ }
+ }
+ },
+ "AWSTemplateFormatVersion" : "2010-09-09",
+ "Description" : "Launches an example cluster",
+ "Mappings" : {
+ "ebs" : {
+ "ap-northeast-1" : {
+ "AMI" : "ami-4e6cd34f"
+ },
+ "ap-southeast-1" : {
+ "AMI" : "ami-a6a7e7f4"
+ },
+ "eu-west-1" : {
+ "AMI" : "ami-c37474b7"
+ },
+ "sa-east-1" : {
+ "AMI" : "ami-1e08d103"
+ },
+ "us-east-1" : {
+ "AMI" : "ami-1624987f"
+ },
+ "us-west-1" : {
+ "AMI" : "ami-1bf9de5e"
+ },
+ "us-west-2" : {
+ "AMI" : "ami-2a31bf1a"
+ }
+ },
+ "ephemeral" : {
+ "ap-northeast-1" : {
+ "AMI" : "ami-5a6cd35b"
+ },
+ "ap-southeast-1" : {
+ "AMI" : "ami-a8a7e7fa"
+ },
+ "eu-west-1" : {
+ "AMI" : "ami-b57474c1"
+ },
+ "sa-east-1" : {
+ "AMI" : "ami-1608d10b"
+ },
+ "us-east-1" : {
+ "AMI" : "ami-e8249881"
+ },
+ "us-west-1" : {
+ "AMI" : "ami-21f9de64"
+ },
+ "us-west-2" : {
+ "AMI" : "ami-2e31bf1e"
+ }
+ }
+ },
+ "Parameters" : {
+ "ClusterSize" : {
+ "Description" : "Number of nodes in the cluster",
+ "Type" : "String"
+ },
+ "DiskType" : {
+ "AllowedValues" : [
+ "ephemeral",
+ "ebs"
+ ],
+ "Default" : "ephemeral",
+ "Description" : "Type of Disk to use ( ephemeral/ebs )",
+ "Type" : "String"
+ },
+ "InstanceType" : {
+ "AllowedValues" : [
+ "t1.micro",
+ "m1.small",
+ "m1.medium",
+ "m1.large",
+ "m1.xlarge",
+ "m2.xlarge",
+ "m2.2xlarge",
+ "m2.4xlarge",
+ "c1.medium",
+ "c1.xlarge",
+ "cc1.4xlarge"
+ ],
+ "ConstraintDescription" : "must be valid instance type. ",
+ "Default" : "m1.large",
+ "Description" : "Type of EC2 instance for cluster",
+ "Type" : "String"
+ },
+ "KeyName" : {
+ "Description" : "Name of an existing EC2 KeyPair to enable SSH access to the cluster",
+ "Type" : "String"
+ }
+ },
+ "Resources" : {
+ "ApplicationWaitCondition" : {
+ "DependsOn" : "ClusterServerGroup",
+ "Properties" : {
+ "Handle" : {
+ "Ref" : "ApplicationWaitHandle"
+ },
+ "Timeout" : "4500"
+ },
+ "Type" : "AWS::CloudFormation::WaitCondition"
+ },
+ "ApplicationWaitHandle" : {
+ "Type" : "AWS::CloudFormation::WaitConditionHandle"
+ },
+ "CFNInitUser" : {
+ "Properties" : {
+ "Path" : "/",
+ "Policies" : [
+ {
+ "PolicyDocument" : {
+ "Statement" : [
+ {
+ "Action" : [
+ "cloudformation:DescribeStackResource",
+ "s3:GetObject"
+ ],
+ "Effect" : "Allow",
+ "Resource" : "*"
+ }
+ ]
+ },
+ "PolicyName" : "AccessForCFNInit"
+ }
+ ]
+ },
+ "Type" : "AWS::IAM::User"
+ },
+ "CFNKeys" : {
+ "Properties" : {
+ "UserName" : {
+ "Ref" : "CFNInitUser"
+ }
+ },
+ "Type" : "AWS::IAM::AccessKey"
+ },
+ "ClusterCommunication1" : {
+ "Properties" : {
+ "FromPort" : "-1",
+ "GroupName" : {
+ "Ref" : "InstanceSecurityGroup"
+ },
+ "IpProtocol" : "icmp",
+ "SourceSecurityGroupName" : {
+ "Ref" : "InstanceSecurityGroup"
+ },
+ "ToPort" : "-1"
+ },
+ "Type" : "AWS::EC2::SecurityGroupIngress"
+ },
+ "ClusterCommunication2" : {
+ "Properties" : {
+ "FromPort" : "1",
+ "GroupName" : {
+ "Ref" : "InstanceSecurityGroup"
+ },
+ "IpProtocol" : "tcp",
+ "SourceSecurityGroupName" : {
+ "Ref" : "InstanceSecurityGroup"
+ },
+ "ToPort" : "65356"
+ },
+ "Type" : "AWS::EC2::SecurityGroupIngress"
+ },
+ "ClusterCommunication3" : {
+ "Properties" : {
+ "FromPort" : "1",
+ "GroupName" : {
+ "Ref" : "InstanceSecurityGroup"
+ },
+ "IpProtocol" : "udp",
+ "SourceSecurityGroupName" : {
+ "Ref" : "InstanceSecurityGroup"
+ },
+ "ToPort" : "65356"
+ },
+ "Type" : "AWS::EC2::SecurityGroupIngress"
+ },
+ "InstanceSecurityGroup" : {
+ "Properties" : {
+ "GroupDescription" : "Enable SSH access via port 22",
+ "SecurityGroupIngress" : [
+ {
+ "CidrIp" : "0.0.0.0/0",
+ "FromPort" : "22",
+ "IpProtocol" : "tcp",
+ "ToPort" : "22"
+ }
+ ]
+ },
+ "Type" : "AWS::EC2::SecurityGroup"
+ },
+ "LaunchConfig" : {
+ "Properties" : {
+ "IamInstanceProfile" : {
+ "Ref" : "RootInstanceProfile"
+ },
+ "ImageId" : {
+ "Fn::FindInMap" : [
+ {
+ "Ref" : "DiskType"
+ },
+ {
+ "Ref" : "AWS::Region"
+ },
+ "AMI"
+ ]
+ },
+ "InstanceType" : {
+ "Ref" : "InstanceType"
+ },
+ "KeyName" : {
+ "Ref" : "KeyName"
+ },
+ "SecurityGroups" : [
+ {
+ "Ref" : "InstanceSecurityGroup"
+ }
+ ],
+ "UserData" : {
+ "Fn::Base64" : {
+ "Fn::Join" : [
+ "\n",
+ [
+ "#!/bin/bash -v",
+ "exec > >(tee /var/log/cfn-data.log|logger -t user-data -s 2>/dev/console) 2>&1",
+ "",
+ "sleep 10",
+ "",
+ "function retry {",
+ " nTrys=0",
+ " maxTrys=5",
+ " status=256",
+ " until [ $status == 0 ] ; do",
+ " $1",
+ " status=$?",
+ " nTrys=$(($nTrys + 1))",
+ " if [ $nTrys -gt $maxTrys ] ; then",
+ " echo \"Number of re-trys exceeded. Exit code: $status\"",
+ " exit $status",
+ " fi",
+ " if [ $status != 0 ] ; then",
+ " echo \"Failed (exit code $status)... retry $nTrys\"",
+ " sleep 10",
+ " fi",
+ " done",
+ "}",
+ "",
+ "yum update -y aws-cfn-bootstrap",
+ "",
+ "#for all the stuff that complains about sudo and tty",
+ "sed -i 's,Defaults requiretty,#Defaults requiretty,g' /etc/sudoers",
+ "",
+ "function error_exit",
+ "{",
+ {
+ "Fn::Join" : [
+ "",
+ [
+ " /opt/aws/bin/cfn-signal -e 1 -r \"$1\" '",
+ {
+ "Ref" : "ApplicationWaitHandle"
+ },
+ "'"
+ ]
+ ]
+ },
+ "}",
+ "yum update -y aws-cfn-bootstrap",
+ "#this runs the first stage of cfinit",
+ {
+ "Fn::Join" : [
+ "",
+ [
+ "#/opt/aws/bin/cfn-init -c ascending -v --region ",
+ {
+ "Ref" : "AWS::Region"
+ },
+ " -s ",
+ {
+ "Ref" : "AWS::StackName"
+ },
+ " -r ",
+ "LaunchConfig",
+ " --access-key ",
+ {
+ "Ref" : "CFNKeys"
+ },
+ " --secret-key ",
+ {
+ "Fn::GetAtt" : [
+ "CFNKeys",
+ "SecretAccessKey"
+ ]
+ },
+ " || error_exit 'Failed to initialize client using cfn-init'"
+ ]
+ ]
+ },
+ "",
+ "",
+ "",
+ "result_code=$?",
+ {
+ "Fn::Join" : [
+ "",
+ [
+ "/opt/aws/bin/cfn-signal -e $result_code '",
+ {
+ "Ref" : "ApplicationWaitHandle"
+ },
+ "'"
+ ]
+ ]
+ }
+ ]
+ ]
+ }
+ }
+ },
+ "Type" : "AWS::AutoScaling::LaunchConfiguration"
+ },
+ "ClusterServerGroup" : {
+ "Properties" : {
+ "AvailabilityZones" : {
+ "Fn::GetAZs" : ""
+ },
+ "LaunchConfigurationName" : {
+ "Ref" : "LaunchConfig"
+ },
+ "MaxSize" : {
+ "Ref" : "ClusterSize"
+ },
+ "MinSize" : {
+ "Ref" : "ClusterSize"
+ }
+ },
+ "Type" : "AWS::AutoScaling::AutoScalingGroup"
+ },
+ "RolePolicies" : {
+ "Properties" : {
+ "PolicyDocument" : {
+ "Statement" : [
+ {
+ "Action" : "*",
+ "Effect" : "Allow",
+ "Resource" : "*"
+ }
+ ]
+ },
+ "PolicyName" : "root",
+ "Roles" : [
+ {
+ "Ref" : "RootRole"
+ }
+ ]
+ },
+ "Type" : "AWS::IAM::Policy"
+ },
+ "RootInstanceProfile" : {
+ "Properties" : {
+ "Path" : "/",
+ "Roles" : [
+ {
+ "Ref" : "RootRole"
+ }
+ ]
+ },
+ "Type" : "AWS::IAM::InstanceProfile"
+ },
+ "RootRole" : {
+ "Properties" : {
+ "AssumeRolePolicyDocument" : {
+ "Statement" : [
+ {
+ "Action" : [
+ "sts:AssumeRole"
+ ],
+ "Effect" : "Allow",
+ "Principal" : {
+ "Service" : [
+ "ec2.amazonaws.com"
+ ]
+ }
+ }
+ ]
+ },
+ "Path" : "/"
+ },
+ "Type" : "AWS::IAM::Role"
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/cloudformation b/library/cloudformation
new file mode 100644
index 0000000000..5ab2d1a93e
--- /dev/null
+++ b/library/cloudformation
@@ -0,0 +1,243 @@
+#!/usr/bin/python -tt
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see .
+
+DOCUMENTATION = '''
+---
+module: cloudformation
+short_description: create a AWS CloudFormation stack
+description:
+ - Launches an AWS CloudFormation stack and waits for it complete.
+version_added: "1.1"
+options:
+ stack_name:
+ description:
+ - name of the cloudformation stack
+ required: true
+ default: null
+ aliases: []
+ disable_rollback:
+ description:
+ - If a stacks fails to form, rollback will remove the stack
+ required: false
+ default: false
+ aliases: []
+ template_parameters:
+ description:
+ - a list of hashes of all the template variables for the stack
+ required: true
+ default: null
+ aliases: []
+ region:
+ description:
+ - The AWS region the stack will be launched in
+ required: true
+ default: null
+ aliases: []
+ state:
+ description:
+ - If state is "present", stack will be created. If state is "present" and if stack exists and template has changed, it will be updated.
+ If state is present, stack will be removed.
+ required: true
+ default: null
+ aliases: []
+ template:
+ description:
+ - the path of the cloudformation template
+ required: true
+ default: null
+ aliases: []
+ wait_for:
+ description:
+ - Wait while the stack is being created/updated/deleted.
+ required: false
+ default: true
+ aliases: []
+
+examples:
+
+ tasks:
+ - name: launch ansible cloudformation example
+ cloudformation: >
+ stack_name="ansible-cloudformation" state=present
+ region=us-east-1 disable_rollback=true
+ template=files/cloudformation-example.json
+ args:
+ template_parameters:
+ KeyName: jmartin
+ DiskType: ephemeral
+ InstanceType: m1.small
+ ClusterSize: 3
+
+requirements: [ "boto" ]
+author: James S. Martin
+'''
+
+import boto.cloudformation.connection
+import json
+
+
+class Region:
+ def __init__(self, region):
+ self.name = region
+ self.endpoint = 'cloudformation.%s.amazonaws.com' % region
+
+
+def boto_exception(err):
+
+ if hasattr(err, 'error_message'):
+ error = err.error_message
+ elif hasattr(err, 'message'):
+ error = err.message
+ else:
+ error = '%s: %s' % (Exception, err)
+ try:
+ error_msg = json.loads(error)
+ except:
+ error_msg = {'Error': error}
+ return error_msg
+
+
+def stack_operation(cfn, stack_name, operation):
+ existed = []
+ result = {}
+ operation_complete = False
+ while operation_complete == False:
+ try:
+ stack = cfn.describe_stacks(stack_name)[0]
+ existed.append('yes')
+ except:
+ if 'yes' in existed:
+ result = {'changed': True, 'output': 'Stack Deleted'}
+ result['events'] = map(str, list(stack.describe_events()))
+ else:
+ result = {'changed': True, 'output': 'Stack Not Found'}
+ break
+ if '%s_COMPLETE' % operation == stack.stack_status:
+ result['changed'] = True
+ result['events'] = map(str, list(stack.describe_events()))
+ result['output'] = 'Stack %s complete' % operation
+ break
+ elif '%s_FAILED' % operation == stack.stack_status:
+ result['changed'] = False
+ result['events'] = map(str, list(stack.describe_events()))
+ result['output'] = 'Stack %s failed' % operation
+ break
+ else:
+ time.sleep(5)
+ return result
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ stack_name=dict(required=True),
+ template_parameters=dict(required=False),
+ region=dict(required=True,
+ choices=['ap-northeast-1', 'ap-southeast-1',
+ 'ap-southeast-2', 'eu-west-1',
+ 'sa-east-1', 'us-east-1', 'us-west-1',
+ 'us-west-2']),
+ state=dict(default='present', choices=['present', 'absent']),
+ template=dict(default=None, required=True),
+ disable_rollback=dict(default=False),
+ wait_for=dict(default=True)
+ )
+ )
+
+ wait_for = module.params['wait_for']
+ state = module.params['state']
+ stack_name = module.params['stack_name']
+ region = Region(module.params['region'])
+ template_body = open(module.params['template'], 'r').read()
+ disable_rollback = module.params['disable_rollback']
+ template_parameters = module.params['template_parameters']
+
+ template_parameters_tup = [(k, v) for k, v in template_parameters.items()]
+ stack_outputs = {}
+ stack_outputs[module.params['region']] = {}
+ stack_outputs[module.params['region']][stack_name] = {}
+
+ try:
+ cfn = boto.cloudformation.connection.CloudFormationConnection(
+ region=region)
+ except boto.exception.NoAuthHandlerFound, e:
+ module.fail_json(msg=str(e))
+ update = False
+ stack_events = []
+ result = {}
+ operation = None
+ output = ''
+
+ if state == 'present':
+ try:
+ cfn.create_stack(stack_name, parameters=template_parameters_tup,
+ template_body=template_body,
+ disable_rollback=disable_rollback,
+ capabilities=['CAPABILITY_IAM'])
+ operation = 'CREATE'
+ except Exception, err:
+ error_msg = boto_exception(err)
+ if error_msg['Error']['Code'] == 'AlreadyExistsException':
+ update = True
+ else:
+ result = {'changed': False, 'output': error_msg}
+ module.fail_json(**result)
+ if not update:
+ result = stack_operation(cfn, stack_name, operation)
+ if update:
+ try:
+ cfn.update_stack(stack_name, parameters=template_parameters_tup,
+ template_body=template_body,
+ disable_rollback=disable_rollback,
+ capabilities=['CAPABILITY_IAM'])
+ operation = 'UPDATE'
+ except Exception, err:
+ error_msg = boto_exception(err)
+ if error_msg['Error']['Message'] == 'No updates are to be performed.':
+ output = error_msg['Error']['Message']
+ result = {'changed': False, 'output': output}
+ if operation == 'UPDATE':
+ result = stack_operation(cfn, stack_name, operation)
+
+ if state == 'present' or update:
+ stack = cfn.describe_stacks(stack_name)[0]
+ for output in stack.outputs:
+ stack_outputs[module.params['region']][stack_name][
+ output.key] = output.value
+ result['stack_outputs'] = stack_outputs
+
+# absent state is different because of the way delete_stack works.
+# problem is it it doesn't give an error if stack isn't found
+# so must describe the stack first
+
+ if state == 'absent':
+ try:
+ cfn.describe_stacks(stack_name)
+ operation = 'DELETE'
+ except Exception, err:
+ error_msg = boto_exception(err)
+ result = {'changed': False, 'output': error_msg}
+ module.fail_json(result)
+ if operation == 'DELETE':
+ cfn.delete_stack(stack_name)
+ result = stack_operation(cfn, stack_name, operation)
+
+ module.exit_json(**result)
+
+# this is magic, see lib/ansible/module_common.py
+#<>
+
+main()