Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create variable number of EC2 instance resources in Cloudformation template?

How to create variable number of EC2 instance resources in Cloudformation template, according to a template parameter?

The EC2 API and management tools allow launching multiple instances of the same AMI, but I can't find how to do this using Cloudformation.

like image 509
nivertech Avatar asked Mar 09 '11 20:03

nivertech


People also ask

How do you create multiple resources in CloudFormation?

Use AWS CloudFormation Macros to create multiple resources from a single resource definition. AWS CloudFormation macros are used for the custom processing of your template. They use the features of imperative programming, which are not natively available while writing CloudFormation templates.

How do I create AWS resources using CloudFormation template?

Create a stack from existing resources using the AWS Management Console. Sign in to the AWS Management Console and open the AWS CloudFormation console at https://console.aws.amazon.com/cloudformation . On the Stacks page, choose Create stack, and then choose With existing resources (import resources).

What is the maximum number of parameters that you can use in an AWS CloudFormation template?

You can have a maximum of 200 parameters in an AWS CloudFormation template. Each parameter must be given a logical name (also called logical ID), which must be alphanumeric and unique among all logical names within the template.


1 Answers

The AWS::EC2::Instance Resource doesn't support the MinCount/MaxCount parameters of the underlying RunInstances API, so it's not possible to create a variable number of EC2 instances by passing Parameters to a single copy of this Resource.

To create a variable number of EC2 instance resources in CloudFormation template according to a template Parameter, and without deploying an Auto Scaling Group instead, there are two options:

1. Conditions

You can use Conditions to create a variable number of AWS::EC2::Instance Resources depending on the Parameter.

It's a little verbose (because you have to use Fn::Equals), but it works.

Here's a working example that allows the user to specify up to a maximum of 5 instances:

Launch Stack

Description: Create a variable number of EC2 instance resources. Parameters:   InstanceCount:     Description: Number of EC2 instances (must be between 1 and 5).     Type: Number     Default: 1     MinValue: 1     MaxValue: 5     ConstraintDescription: Must be a number between 1 and 5.   ImageId:     Description: Image ID to launch EC2 instances.     Type: AWS::EC2::Image::Id     # amzn-ami-hvm-2016.09.1.20161221-x86_64-gp2     Default: ami-9be6f38c   InstanceType:     Description: Instance type to launch EC2 instances.     Type: String     Default: m3.medium     AllowedValues: [ m3.medium, m3.large, m3.xlarge, m3.2xlarge ] Conditions:   Launch1: !Equals [1, 1]   Launch2: !Not [!Equals [1, !Ref InstanceCount]]   Launch3: !And   - !Not [!Equals [1, !Ref InstanceCount]]   - !Not [!Equals [2, !Ref InstanceCount]]   Launch4: !Or   - !Equals [4, !Ref InstanceCount]   - !Equals [5, !Ref InstanceCount]   Launch5: !Equals [5, !Ref InstanceCount] Resources:   Instance1:     Condition: Launch1     Type: AWS::EC2::Instance     Properties:       ImageId: !Ref ImageId       InstanceType: !Ref InstanceType   Instance2:     Condition: Launch2     Type: AWS::EC2::Instance     Properties:       ImageId: !Ref ImageId       InstanceType: !Ref InstanceType   Instance3:     Condition: Launch3     Type: AWS::EC2::Instance     Properties:       ImageId: !Ref ImageId       InstanceType: !Ref InstanceType   Instance4:     Condition: Launch4     Type: AWS::EC2::Instance     Properties:       ImageId: !Ref ImageId       InstanceType: !Ref InstanceType   Instance5:     Condition: Launch5     Type: AWS::EC2::Instance     Properties:       ImageId: !Ref ImageId       InstanceType: !Ref InstanceType 

1a. Template preprocessor with Conditions

As a variation on the above, you can use a template preprocessor like Ruby's Erb to generate the above template based on a specified maximum, making your source code more compact and eliminating duplication:

<%max = 10-%> Description: Create a variable number of EC2 instance resources. Parameters:   InstanceCount:     Description: Number of EC2 instances (must be between 1 and <%=max%>).     Type: Number     Default: 1     MinValue: 1     MaxValue: <%=max%>     ConstraintDescription: Must be a number between 1 and <%=max%>.   ImageId:     Description: Image ID to launch EC2 instances.     Type: AWS::EC2::Image::Id     # amzn-ami-hvm-2016.09.1.20161221-x86_64-gp2     Default: ami-9be6f38c   InstanceType:     Description: Instance type to launch EC2 instances.     Type: String     Default: m3.medium     AllowedValues: [ m3.medium, m3.large, m3.xlarge, m3.2xlarge ] Conditions:   Launch1: !Equals [1, 1]   Launch2: !Not [!Equals [1, !Ref InstanceCount]] <%(3..max-1).each do |x|     low = (max-1)/(x-1) <= 1-%>   Launch<%=x%>: !<%=low ? 'Or' : 'And'%> <%  (1..max).each do |i|       if low && i >= x-%>   - !Equals [<%=i%>, !Ref InstanceCount] <%    elsif !low && i < x-%>   - !Not [!Equals [<%=i%>, !Ref InstanceCount]] <%    end     end   end-%>   Launch<%=max%>: !Equals [<%=max%>, !Ref InstanceCount] Resources: <%(1..max).each do |x|-%>   Instance<%=x%>:     Condition: Launch<%=x%>     Type: AWS::EC2::Instance     Properties:       ImageId: !Ref ImageId       InstanceType: !Ref InstanceType <%end-%> 

To process the above source into a CloudFormation-compatible template, run:

ruby -rerb -e "puts ERB.new(ARGF.read, nil, '-').result" < template.yml > template-out.yml 

For convenience, here is a gist with the generated output YAML for 10 variable EC2 instances.

2. Custom Resource

An alternate approach is to implement a Custom Resource that calls the RunInstances/TerminateInstances APIs directly:

Launch Stack

Description: Create a variable number of EC2 instance resources. Parameters:   InstanceCount:     Description: Number of EC2 instances (must be between 1 and 10).     Type: Number     Default: 1     MinValue: 1     MaxValue: 10     ConstraintDescription: Must be a number between 1 and 10.   ImageId:     Description: Image ID to launch EC2 instances.     Type: AWS::EC2::Image::Id     # amzn-ami-hvm-2016.09.1.20161221-x86_64-gp2     Default: ami-9be6f38c   InstanceType:     Description: Instance type to launch EC2 instances.     Type: String     Default: m3.medium     AllowedValues: [ m3.medium, m3.large, m3.xlarge, m3.2xlarge ] Resources:   EC2Instances:     Type: Custom::EC2Instances     Properties:       ServiceToken: !GetAtt EC2InstancesFunction.Arn       ImageId: !Ref ImageId       InstanceType: !Ref InstanceType       MinCount: !Ref InstanceCount       MaxCount: !Ref InstanceCount   EC2InstancesFunction:     Type: AWS::Lambda::Function     Properties:       Handler: index.handler       Role: !GetAtt LambdaExecutionRole.Arn       Code:         ZipFile: !Sub |           var response = require('cfn-response');           var AWS = require('aws-sdk');           exports.handler = function(event, context) {             var physicalId = event.PhysicalResourceId || 'none';             function success(data) {               return response.send(event, context, response.SUCCESS, data, physicalId);             }             function failed(e) {               return response.send(event, context, response.FAILED, e, physicalId);             }             var ec2 = new AWS.EC2();             var instances;             if (event.RequestType == 'Create') {               var launchParams = event.ResourceProperties;               delete launchParams.ServiceToken;               ec2.runInstances(launchParams).promise().then((data)=> {                 instances = data.Instances.map((data)=> data.InstanceId);                 physicalId = instances.join(':');                 return ec2.waitFor('instanceRunning', {InstanceIds: instances}).promise();               }).then((data)=> success({Instances: instances})               ).catch((e)=> failed(e));             } else if (event.RequestType == 'Delete') {               if (physicalId == 'none') {return success({});}               var deleteParams = {InstanceIds: physicalId.split(':')};               ec2.terminateInstances(deleteParams).promise().then((data)=>                 ec2.waitFor('instanceTerminated', deleteParams).promise()               ).then((data)=>success({})               ).catch((e)=>failed(e));             } else {               return failed({Error: "In-place updates not supported."});             }           };       Runtime: nodejs4.3       Timeout: 300   LambdaExecutionRole:     Type: AWS::IAM::Role     Properties:       AssumeRolePolicyDocument:         Version: '2012-10-17'         Statement:         - Effect: Allow           Principal: {Service: [lambda.amazonaws.com]}           Action: ['sts:AssumeRole']       Path: /       ManagedPolicyArns:       - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole       Policies:       - PolicyName: EC2Policy         PolicyDocument:           Version: '2012-10-17'           Statement:             - Effect: Allow               Action:               - 'ec2:RunInstances'               - 'ec2:DescribeInstances'               - 'ec2:DescribeInstanceStatus'               - 'ec2:TerminateInstances'               Resource: ['*'] Outputs:   Instances:     Value: !Join [',', !GetAtt EC2Instances.Instances] 
like image 123
wjordan Avatar answered Sep 22 '22 14:09

wjordan