Merge pull request #2180 from jsmartin/cloudformation

CloudFormation support.
This commit is contained in:
Michael DeHaan 2013-02-23 09:57:09 -08:00
commit 69a199727c
3 changed files with 687 additions and 0 deletions

View file

@ -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}"

View file

@ -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"
}
}
}

243
library/cloudformation Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()