Skip to content
This repository was archived by the owner on Jan 9, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cmd/tarmak/cmd/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ func clusterApplyFlags(fs *flag.FlagSet) {
false,
"apply changes to infrastructure only, by running only terraform",
)

fs.BoolVarP(
&store.SpotPricing,
"spot-pricing",
"P",
false,
"Use a best effort spot pricing based on last 3 days pricing for instance pools",
)

}

func clusterDestroyFlags(fs *flag.FlagSet) {
Expand Down
34 changes: 34 additions & 0 deletions docs/user-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,39 @@ policy permissions. `Information on how to correctly set these permissions can
be found here
<https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/enable-access-logs.html#attach-bucket-policy>`_.

Spot Instances
~~~~~~~~~~~~~~
Tarmak gives the ability to attempt to request spot instances for use in
instance pools. Spot instances are very cheap, spare AWS instances that can be
revoked by AWS at any time, given a 2 minute notification. `More information
here <https://aws.amazon.com/ec2/spot/>`_.

Spot instances can be requested cluster-wide by giving the ``--spot-pricing``
flag to ``cluster apply``. Tarmak will then attempt a best effort spot price for
each instance pool in the cluster, calculated as the average spot price in the
last 3 days for that instance type in each zone plus 25%.

Manual spot prices can be applied to each instance pool within the
``tarmak.yaml`` which will override the Tarmak best effort for that instance
pool. This is done through the ``spotPrice`` attribute under the instance pool,
given as a number in USD. This can be added like so:

.. code-block:: yaml

- image: centos-puppet-agent
maxCount: 3
metadata:
creationTimestamp: "2018-07-27T09:33:15Z"
name: worker
minCount: 3
size: medium
spotPrice: 0.015


Note that Tarmak will only attempt to create spot instances for instance pools
with the ``spotPrice`` attribute or spot pricing flag during a cluster apply.


Cluster Services
----------------

Expand All @@ -721,3 +754,4 @@ Do the following steps to access Grafana:
.. code-block:: none

http://127.0.0.1:8001/api/v1/namespaces/kube-system/services/monitoring-grafana/proxy/

2 changes: 2 additions & 0 deletions pkg/apis/tarmak/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ type ClusterApplyFlags struct {
InfrastructureOnly bool // only run terraform

ConfigurationOnly bool // only run puppet

SpotPricing bool // Use spot pricing for all applied instances at best effort according to last 3 days pricing. Spot prices in config will override this option per instance pool.
}

// Contains the cluster destroy flags
Expand Down
22 changes: 19 additions & 3 deletions pkg/tarmak/instance_pool/instance_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type InstancePool struct {
rootVolume *Volume

instanceType string
spotPrice string

role *role.Role
}
Expand Down Expand Up @@ -77,7 +78,7 @@ func NewFromConfig(cluster interfaces.Cluster, conf *clusterv1alpha1.InstancePoo
return nil, fmt.Errorf("minCount does not equal maxCount but role is stateful. minCount=%d maxCount=%d", instancePool.Config().MinCount, instancePool.Config().MaxCount)
}

var result error
var result *multierror.Error

count := 0
for pos, _ := range conf.Volumes {
Expand All @@ -98,7 +99,9 @@ func NewFromConfig(cluster interfaces.Cluster, conf *clusterv1alpha1.InstancePoo
return nil, errors.New("no root volume given")
}

return instancePool, result
instancePool.spotPrice = conf.SpotPrice

return instancePool, result.ErrorOrNil()
}

func (n *InstancePool) Role() *role.Role {
Expand Down Expand Up @@ -166,7 +169,20 @@ func (n *InstancePool) InstanceType() string {
}

func (n *InstancePool) SpotPrice() string {
return n.conf.SpotPrice
return n.spotPrice
}

func (n *InstancePool) CalculateSpotPrice() error {
if n.spotPrice == "" {
p, err := n.cluster.Environment().Provider().SpotPrice(n)
if err != nil {
return err
}

n.spotPrice = fmt.Sprint(p)
}

return nil
}

func (n *InstancePool) AmazonAdditionalIAMPolicies() string {
Expand Down
2 changes: 2 additions & 0 deletions pkg/tarmak/interfaces/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ type Provider interface {
AskInstancePoolZones(Initialize) (zones []string, err error)
UploadConfiguration(Cluster, io.ReadSeeker) error
VerifyInstanceTypes(intstancePools []InstancePool) error
SpotPrice(instancePool InstancePool) (float64, error)
}

type Tarmak interface {
Expand Down Expand Up @@ -254,6 +255,7 @@ type InstancePool interface {
InstanceType() string
Labels() (string, error)
Taints() (string, error)
CalculateSpotPrice() error
}

type Volume interface {
Expand Down
74 changes: 74 additions & 0 deletions pkg/tarmak/provider/amazon/spot_price.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright Jetstack Ltd. See LICENSE for details.
package amazon

import (
"fmt"
"strconv"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/go-multierror"

"github.com/jetstack/tarmak/pkg/tarmak/interfaces"
)

func (a *Amazon) SpotPrice(instancePool interfaces.InstancePool) (float64, error) {
sess, err := a.Session()
if err != nil {
return 0, err
}

svc := ec2.New(sess)

instanceType, err := a.InstanceType(instancePool.Config().Size)
if err != nil {
return 0, err
}

timeToColect := time.Now().Add(-24 * 3 * time.Hour)

var result *multierror.Error
var prices []float64
for _, zone := range instancePool.Zones() {
spotPriceRequestInput := &ec2.DescribeSpotPriceHistoryInput{
InstanceTypes: []*string{
aws.String(instanceType),
},
ProductDescriptions: []*string{
aws.String("Linux/UNIX"),
},
AvailabilityZone: aws.String(zone),
StartTime: &timeToColect,
}

output, err := svc.DescribeSpotPriceHistory(spotPriceRequestInput)
if err != nil {
result = multierror.Append(result, fmt.Errorf("failed to get current spot price of instance type '%s': %v", instanceType, err))
continue
}

for _, entry := range output.SpotPriceHistory {
price, err := strconv.ParseFloat(*entry.SpotPrice, 64)
if err != nil {
result = multierror.Append(result, fmt.Errorf("failed to parse spot price '%s': %v", *entry.SpotPrice, err))
continue
}

prices = append(prices, price)
}
}

var total float64
for _, price := range prices {
total += price
}

if len(prices) != 0 {
total /= float64(len(prices))
}

total *= 1.25

return total, result.ErrorOrNil()
}
17 changes: 17 additions & 0 deletions pkg/tarmak/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import (
"strings"

"github.com/blang/semver"
"github.com/hashicorp/go-multierror"
terraformVersion "github.com/hashicorp/terraform/version"

"github.com/jetstack/tarmak/pkg/tarmak/interfaces"
)

Expand Down Expand Up @@ -56,6 +58,21 @@ func (t *Tarmak) CmdTerraformApply(args []string, ctx context.Context) error {
return err
}

if t.flags.Cluster.Apply.SpotPricing {
t.cluster.Log().Info("calculating instance pool spot prices")

var result *multierror.Error
for _, i := range t.Cluster().InstancePools() {
if err := i.CalculateSpotPrice(); err != nil {
result = multierror.Append(result, err)
}
}

if result != nil {
return result.ErrorOrNil()
}
}

t.cluster.Log().Info("write SSH config")
if err := t.writeSSHConfigForClusterHosts(); err != nil {
return err
Expand Down