Skip to content

Commit fb37e03

Browse files
vyncentkPettitWesley
authored andcommitted
Add support to EC2 Spot instances
1 parent e4ee1d8 commit fb37e03

File tree

6 files changed

+80
-0
lines changed

6 files changed

+80
-0
lines changed

ecs-cli/modules/cli/cluster/cluster_app.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func init() {
5858
flags.KeypairNameFlag: cloudformation.ParameterKeyKeyPairName,
5959
flags.ImageIdFlag: cloudformation.ParameterKeyAmiId,
6060
flags.InstanceRoleFlag: cloudformation.ParameterKeyInstanceRole,
61+
flags.SpotPriceFlag: cloudformation.ParameterKeySpotPrice,
6162
}
6263
}
6364

ecs-cli/modules/cli/cluster/cluster_app_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,49 @@ func TestClusterUpWithUserData(t *testing.T) {
262262
assert.ElementsMatch(t, []string{"some_file", "some_file2"}, userdataMock.files, "Expected userdata file list to match")
263263
}
264264

265+
func TestClusterUpWithSpotPrice(t *testing.T) {
266+
defer os.Clearenv()
267+
mockECS, mockCloudformation, mockSSM := setupTest(t)
268+
awsClients := &AWSClients{mockECS, mockCloudformation, mockSSM}
269+
270+
spotPrice := "0.03"
271+
272+
gomock.InOrder(
273+
mockECS.EXPECT().CreateCluster(clusterName).Return(clusterName, nil),
274+
)
275+
276+
gomock.InOrder(
277+
mockSSM.EXPECT().GetRecommendedECSLinuxAMI().Return(amiMetadata(amiID), nil),
278+
)
279+
280+
gomock.InOrder(
281+
mockCloudformation.EXPECT().ValidateStackExists(stackName).Return(errors.New("error")),
282+
mockCloudformation.EXPECT().CreateStack(gomock.Any(), stackName, gomock.Any()).Do(func(x, y, z interface{}) {
283+
cfnParams := z.(*cloudformation.CfnStackParams)
284+
param, err := cfnParams.GetParameter(cloudformation.ParameterKeySpotPrice)
285+
assert.NoError(t, err, "Expected Spot Price parameter to be set")
286+
assert.Equal(t, spotPrice, aws.StringValue(param.ParameterValue), "Expected spot price to match")
287+
}).Return("", nil),
288+
mockCloudformation.EXPECT().WaitUntilCreateComplete(stackName).Return(nil),
289+
)
290+
291+
globalSet := flag.NewFlagSet("ecs-cli", 0)
292+
globalContext := cli.NewContext(nil, globalSet, nil)
293+
294+
flagSet := flag.NewFlagSet("ecs-cli-up", 0)
295+
flagSet.Bool(flags.CapabilityIAMFlag, true, "")
296+
flagSet.String(flags.KeypairNameFlag, "default", "")
297+
flagSet.String(flags.SpotPriceFlag, spotPrice, "")
298+
299+
context := cli.NewContext(nil, flagSet, globalContext)
300+
rdwr := newMockReadWriter()
301+
commandConfig, err := newCommandConfig(context, rdwr)
302+
assert.NoError(t, err, "Unexpected error creating CommandConfig")
303+
304+
err = createCluster(context, awsClients, commandConfig)
305+
assert.NoError(t, err, "Unexpected error bringing up cluster")
306+
}
307+
265308
func TestClusterUpWithVPC(t *testing.T) {
266309
defer os.Clearenv()
267310
mockECS, mockCloudformation, mockSSM := setupTest(t)

ecs-cli/modules/clients/aws/cloudformation/params.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const (
3838
ParameterKeyInstanceRole = "InstanceRole"
3939
ParameterKeyIsFargate = "IsFargate"
4040
ParameterKeyUserData = "UserData"
41+
ParameterKeySpotPrice = "SpotPrice"
4142
)
4243

4344
var ParameterNotFoundError = errors.New("Parameter not found")
@@ -72,6 +73,7 @@ func init() {
7273
ParameterKeyCluster,
7374
ParameterKeyAmiId,
7475
ParameterKeyAssociatePublicIPAddress,
76+
ParameterKeySpotPrice,
7577
}
7678
}
7779

ecs-cli/modules/clients/aws/cloudformation/template.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ var template = `
184184
],
185185
"ConstraintDescription": "must be a valid EC2 instance type."
186186
},
187+
"SpotPrice": {
188+
"Type": "Number",
189+
"Description": "If greater than 0, then a EC2 Spot instance will be requested",
190+
"Default": "0"
191+
},
187192
"KeyName": {
188193
"Type": "String",
189194
"Description": "Optional - Name of an existing EC2 KeyPair to enable SSH access to the ECS instances",
@@ -340,6 +345,18 @@ var template = `
340345
]
341346
}
342347
]
348+
},
349+
"UseSpotInstances": {
350+
"Fn::Not": [
351+
{
352+
"Fn::Equals": [
353+
{
354+
"Ref": "SpotPrice"
355+
},
356+
0
357+
]
358+
}
359+
]
343360
}
344361
},
345362
"Resources": {
@@ -565,6 +582,17 @@ var template = `
565582
"InstanceType": {
566583
"Ref": "EcsInstanceType"
567584
},
585+
"SpotPrice": {
586+
"Fn::If": [
587+
"UseSpotInstances",
588+
{
589+
"Ref": "SpotPrice"
590+
},
591+
{
592+
"Ref": "AWS::NoValue"
593+
}
594+
]
595+
},
568596
"AssociatePublicIpAddress": {
569597
"Ref": "AssociatePublicIpAddress"
570598
},

ecs-cli/modules/commands/cluster/cluster_command.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ func clusterUpFlags() []cli.Flag {
8686
Name: flags.InstanceTypeFlag,
8787
Usage: "[Optional] Specifies the EC2 instance type for your container instances. Defaults to t2.micro. NOTE: Not applicable for launch type FARGATE.",
8888
},
89+
cli.StringFlag{
90+
Name: flags.SpotPriceFlag,
91+
Usage: "[Optional] If filled and greater than 0, EC2 Spot instances will be requested.",
92+
},
8993
cli.StringFlag{
9094
Name: flags.ImageIdFlag,
9195
Usage: "[Optional] Specify the AMI ID for your container instances. Defaults to amazon-ecs-optimized AMI. NOTE: Not applicable for launch type FARGATE.",

ecs-cli/modules/commands/flags/flags.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const (
7777
SubnetIdsFlag = "subnets"
7878
VpcIdFlag = "vpc"
7979
InstanceTypeFlag = "instance-type"
80+
SpotPriceFlag = "spot-price"
8081
InstanceRoleFlag = "instance-role"
8182
ImageIdFlag = "image-id"
8283
KeypairNameFlag = "keypair"
@@ -221,5 +222,6 @@ func CFNResourceFlags() []string {
221222
InstanceRoleFlag,
222223
ImageIdFlag,
223224
KeypairNameFlag,
225+
SpotPriceFlag,
224226
}
225227
}

0 commit comments

Comments
 (0)