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.
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.
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).
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.
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:
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:
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
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.
An alternate approach is to implement a Custom Resource that calls the RunInstances
/TerminateInstances
APIs directly:
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]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With