Skip to content

Add AWS resource pricing to cluster up confirmation message #647

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Dec 12, 2019
53 changes: 52 additions & 1 deletion cli/cmd/lib_cluster_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"

"github.com/cortexlabs/cortex/pkg/consts"
"github.com/cortexlabs/cortex/pkg/lib/aws"
"github.com/cortexlabs/cortex/pkg/lib/clusterconfig"
cr "github.com/cortexlabs/cortex/pkg/lib/configreader"
"github.com/cortexlabs/cortex/pkg/lib/errors"
Expand Down Expand Up @@ -226,6 +227,9 @@ func confirmClusterConfig(clusterConfig *clusterconfig.ClusterConfig, awsCreds *
if clusterConfig.InstanceVolumeSize != defaultConfig.InstanceVolumeSize {
items.Add(clusterconfig.InstanceVolumeSizeUserFacingKey, clusterConfig.InstanceVolumeSize)
}
if clusterConfig.ClusterName != defaultConfig.ClusterName {
items.Add(clusterconfig.ClusterNameUserFacingKey, clusterConfig.ClusterName)
}
if clusterConfig.LogGroup != defaultConfig.LogGroup {
items.Add(clusterconfig.LogGroupUserFacingKey, clusterConfig.LogGroup)
}
Expand Down Expand Up @@ -291,9 +295,56 @@ func confirmClusterConfig(clusterConfig *clusterconfig.ClusterConfig, awsCreds *
items.Add(clusterconfig.ImageIstioGalleyUserFacingKey, clusterConfig.ImageIstioGalley)
}

var spotPrice float64
if clusterConfig.Spot != nil && *clusterConfig.Spot {
var err error
spotPrice, err = aws.SpotInstancePrice(awsCreds.AWSAccessKeyID, awsCreds.CortexAWSSecretAccessKey, *clusterConfig.Region, *clusterConfig.InstanceType)
if err != nil {
spotPrice = 0
}
}

items.Print()
fmt.Println()

fmt.Printf("cortex will use your %s aws access key id to provision the following resources in the %s region of your aws account:\n\n", s.MaskString(awsCreds.AWSAccessKeyID, 4), *clusterConfig.Region)
fmt.Printf("○ an s3 bucket named %s\n", *clusterConfig.Bucket)
fmt.Printf("○ a cloudwatch log group named %s\n", clusterConfig.LogGroup)
fmt.Printf("○ an eks cluster named %s ($0.20 per hour)\n", clusterConfig.ClusterName)
fmt.Printf("○ a t3.medium ec2 instance for the operator ($%s per hour)\n", s.Float64(aws.InstanceMetadatas[*clusterConfig.Region]["t3.medium"].Price))
fmt.Printf("○ a 20gb ebs volume for the operator ($%s per hour)\n", s.Round(aws.EBSMetadatas[*clusterConfig.Region].Price*20/30, 3, false))
fmt.Printf("○ an elb for the operator and an elb for apis ($%s per hour each)\n", s.Float64(aws.ELBMetadatas[*clusterConfig.Region].Price))
fmt.Printf("○ a nat gateway ($%s per hour)\n", s.Float64(aws.NATMetadatas[*clusterConfig.Region].Price))
fmt.Println(workloadInstancesStr(clusterConfig, spotPrice))

fmt.Println()

exitMessage := fmt.Sprintf("cluster configuration can be modified via the cluster config file; see https://www.cortex.dev/v/%s/cluster-management/config", consts.CortexVersionMinor)
prompt.YesOrExit("is the configuration above correct?", exitMessage)
prompt.YesOrExit("would you like to continue with this installation?", exitMessage)
}

func workloadInstancesStr(clusterConfig *clusterconfig.ClusterConfig, spotPrice float64) string {
instanceRangeStr := fmt.Sprintf("an autoscaling group of %d - %d", *clusterConfig.MinInstances, *clusterConfig.MaxInstances)
if *clusterConfig.MinInstances == *clusterConfig.MaxInstances {
instanceRangeStr = s.Int64(*clusterConfig.MinInstances)
}

instancesStr := "instances"
if *clusterConfig.MinInstances == 1 && *clusterConfig.MaxInstances == 1 {
instancesStr = "instance"
}

instanceTypeStr := *clusterConfig.InstanceType
instancePriceStr := fmt.Sprintf("($%s per hour each)", s.Float64(aws.InstanceMetadatas[*clusterConfig.Region][*clusterConfig.InstanceType].Price))

if clusterConfig.Spot != nil && *clusterConfig.Spot {
instanceTypeStr = s.StrsOr(clusterConfig.SpotConfig.InstanceDistribution)
spotPriceStr := "spot pricing not available"
if spotPrice != 0 {
spotPriceStr = fmt.Sprintf("~$%s per hour spot", s.Float64(spotPrice))
}
instancePriceStr = fmt.Sprintf("(%s: $%s per hour on-demand, %s)", *clusterConfig.InstanceType, s.Float64(aws.InstanceMetadatas[*clusterConfig.Region][*clusterConfig.InstanceType].Price), spotPriceStr)
}

return fmt.Sprintf("○ %s %s ec2 %s for apis %s", instanceRangeStr, instanceTypeStr, instancesStr, instancePriceStr)
}
4 changes: 2 additions & 2 deletions pkg/lib/aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

//go:generate python3 gen_instance_metadata.py
//go:generate gofmt -s -w instance_metadata.go
//go:generate python3 gen_resource_metadata.py
//go:generate gofmt -s -w resource_metadata.go

package aws

Expand Down
76 changes: 76 additions & 0 deletions pkg/lib/aws/ec2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
Copyright 2019 Cortex Labs, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package aws

import (
"math"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/cortexlabs/cortex/pkg/lib/errors"
s "github.com/cortexlabs/cortex/pkg/lib/strings"
)

// Returns the minimum of all AZs in the region, 0 if unable to retreive spot price for any reason
func SpotInstancePrice(accessKeyID string, secretAccessKey string, region string, instanceType string) (float64, error) {
sess, err := session.NewSession(&aws.Config{
Region: aws.String(region),
DisableSSL: aws.Bool(false),
Credentials: credentials.NewStaticCredentials(accessKeyID, secretAccessKey, ""),
})
if err != nil {
return 0, errors.WithStack(err)
}

svc := ec2.New(sess)

result, err := svc.DescribeSpotPriceHistory(&ec2.DescribeSpotPriceHistoryInput{
InstanceTypes: []*string{aws.String(instanceType)},
ProductDescriptions: []*string{aws.String("Linux/UNIX")},
StartTime: aws.Time(time.Now()),
})

min := math.MaxFloat64

for _, spotPrice := range result.SpotPriceHistory {
if spotPrice == nil {
continue
}

price, ok := s.ParseFloat64(*spotPrice.SpotPrice)
if !ok {
continue
}

if price < min {
min = price
}
}

if min == math.MaxFloat64 {
return 0, ErrorNoValidSpotPrices(instanceType, region)
}

if min <= 0 {
return 0, ErrorNoValidSpotPrices(instanceType, region)
}

return min, nil
}
9 changes: 9 additions & 0 deletions pkg/lib/aws/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
ErrAuth
ErrBucketInaccessible
ErrPFamilyInstanceUseNotPermitted
ErrNoValidSpotPrices
ErrReadCredentials
)

Expand All @@ -44,6 +45,7 @@ var errorKinds = []string{
"err_auth",
"err_bucket_inaccessible",
"err_p_family_instance_use_not_permitted",
"err_no_valid_spot_prices",
"err_read_credentials",
}

Expand Down Expand Up @@ -154,6 +156,13 @@ func ErrorPFamilyInstanceUseNotPermitted(region string) error {
}
}

func ErrorNoValidSpotPrices(instanceType string, region string) error {
return Error{
Kind: ErrNoValidSpotPrices,
message: fmt.Sprintf("no spot prices were found for %s instances in %s", instanceType, region),
}
}

func ErrorReadCredentials() error {
return Error{
Kind: ErrReadCredentials,
Expand Down
Loading