provider/aws: spot_instance_request
This is an iteration on the great work done by @dalehamel in PRs #2095
and #2109.
The core team went back and forth on how to best model Spot Instance
Requests, requesting and then rejecting a separate-resource
implementation in #2109.
After more internal discussion, we landed once again on a separate
resource to model Spot Instance Requests. Out of respect for
@dalehamel's already-significant donated time, with this I'm attempting
to pick up the work to take this across the finish line.
Important architectural decisions represented here:
* Spot Instance Requests are always of type "persistent", to properly
match Terraform's declarative model.
* The spot_instance_request resource exports several attributes that
are expected to be constantly changing as the spot market changes:
spot_bid_status, spot_request_state, and instance_id. Creating
additional resource dependencies based on these attributes is not
recommended, as Terraform diffs will be continually generated to keep
up with the live changes.
* When a Spot Instance Request is deleted/canceled, an attempt is made
to terminate the last-known attached spot instance. Race conditions
dictate that this attempt cannot guarantee that the associated spot
instance is terminated immediately.
Implementation notes:
* This version of aws_spot_instance_request borrows a lot of common
code from aws_instance.
* In order to facilitate borrowing, we introduce `awsInstanceOpts`, an
internal representation of instance details that's meant to be shared
between resources. The goal here would be to refactor ASG Launch
Configurations to use the same struct.
* The new aws_spot_instance_request acc. test is passing.
* All aws_instance acc. tests remain passing.
2015-06-05 17:12:09 +02:00
|
|
|
package aws
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
|
|
|
"github.com/aws/aws-sdk-go/aws/awsutil"
|
|
|
|
"github.com/aws/aws-sdk-go/service/ec2"
|
|
|
|
"github.com/hashicorp/terraform/helper/resource"
|
|
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
|
|
)
|
|
|
|
|
|
|
|
func resourceAwsSpotInstanceRequest() *schema.Resource {
|
|
|
|
return &schema.Resource{
|
|
|
|
Create: resourceAwsSpotInstanceRequestCreate,
|
|
|
|
Read: resourceAwsSpotInstanceRequestRead,
|
|
|
|
Delete: resourceAwsSpotInstanceRequestDelete,
|
|
|
|
Update: resourceAwsSpotInstanceRequestUpdate,
|
|
|
|
|
|
|
|
Schema: func() map[string]*schema.Schema {
|
|
|
|
// The Spot Instance Request Schema is based on the AWS Instance schema.
|
|
|
|
s := resourceAwsInstance().Schema
|
|
|
|
|
|
|
|
// Everything on a spot instance is ForceNew except tags
|
|
|
|
for k, v := range s {
|
|
|
|
if k == "tags" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
v.ForceNew = true
|
|
|
|
}
|
|
|
|
|
|
|
|
s["spot_price"] = &schema.Schema{
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Required: true,
|
|
|
|
ForceNew: true,
|
|
|
|
}
|
|
|
|
s["wait_for_fulfillment"] = &schema.Schema{
|
|
|
|
Type: schema.TypeBool,
|
|
|
|
Optional: true,
|
|
|
|
Default: false,
|
|
|
|
}
|
|
|
|
s["spot_bid_status"] = &schema.Schema{
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Computed: true,
|
|
|
|
}
|
|
|
|
s["spot_request_state"] = &schema.Schema{
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Computed: true,
|
|
|
|
}
|
|
|
|
s["spot_instance_id"] = &schema.Schema{
|
|
|
|
Type: schema.TypeString,
|
|
|
|
Computed: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
return s
|
|
|
|
}(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func resourceAwsSpotInstanceRequestCreate(d *schema.ResourceData, meta interface{}) error {
|
|
|
|
conn := meta.(*AWSClient).ec2conn
|
|
|
|
|
|
|
|
instanceOpts, err := buildAwsInstanceOpts(d, meta)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
spotOpts := &ec2.RequestSpotInstancesInput{
|
|
|
|
SpotPrice: aws.String(d.Get("spot_price").(string)),
|
|
|
|
|
|
|
|
// We always set the type to "persistent", since the imperative-like
|
|
|
|
// behavior of "one-time" does not map well to TF's declarative domain.
|
|
|
|
Type: aws.String("persistent"),
|
|
|
|
|
|
|
|
// Though the AWS API supports creating spot instance requests for multiple
|
|
|
|
// instances, for TF purposes we fix this to one instance per request.
|
|
|
|
// Users can get equivalent behavior out of TF's "count" meta-parameter.
|
|
|
|
InstanceCount: aws.Long(1),
|
|
|
|
|
|
|
|
LaunchSpecification: &ec2.RequestSpotLaunchSpecification{
|
|
|
|
BlockDeviceMappings: instanceOpts.BlockDeviceMappings,
|
|
|
|
EBSOptimized: instanceOpts.EBSOptimized,
|
2015-06-25 16:58:28 +02:00
|
|
|
Monitoring: instanceOpts.Monitoring,
|
provider/aws: spot_instance_request
This is an iteration on the great work done by @dalehamel in PRs #2095
and #2109.
The core team went back and forth on how to best model Spot Instance
Requests, requesting and then rejecting a separate-resource
implementation in #2109.
After more internal discussion, we landed once again on a separate
resource to model Spot Instance Requests. Out of respect for
@dalehamel's already-significant donated time, with this I'm attempting
to pick up the work to take this across the finish line.
Important architectural decisions represented here:
* Spot Instance Requests are always of type "persistent", to properly
match Terraform's declarative model.
* The spot_instance_request resource exports several attributes that
are expected to be constantly changing as the spot market changes:
spot_bid_status, spot_request_state, and instance_id. Creating
additional resource dependencies based on these attributes is not
recommended, as Terraform diffs will be continually generated to keep
up with the live changes.
* When a Spot Instance Request is deleted/canceled, an attempt is made
to terminate the last-known attached spot instance. Race conditions
dictate that this attempt cannot guarantee that the associated spot
instance is terminated immediately.
Implementation notes:
* This version of aws_spot_instance_request borrows a lot of common
code from aws_instance.
* In order to facilitate borrowing, we introduce `awsInstanceOpts`, an
internal representation of instance details that's meant to be shared
between resources. The goal here would be to refactor ASG Launch
Configurations to use the same struct.
* The new aws_spot_instance_request acc. test is passing.
* All aws_instance acc. tests remain passing.
2015-06-05 17:12:09 +02:00
|
|
|
IAMInstanceProfile: instanceOpts.IAMInstanceProfile,
|
|
|
|
ImageID: instanceOpts.ImageID,
|
|
|
|
InstanceType: instanceOpts.InstanceType,
|
|
|
|
Placement: instanceOpts.SpotPlacement,
|
|
|
|
SecurityGroupIDs: instanceOpts.SecurityGroupIDs,
|
|
|
|
SecurityGroups: instanceOpts.SecurityGroups,
|
|
|
|
UserData: instanceOpts.UserData64,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make the spot instance request
|
|
|
|
resp, err := conn.RequestSpotInstances(spotOpts)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Error requesting spot instances: %s", err)
|
|
|
|
}
|
|
|
|
if len(resp.SpotInstanceRequests) != 1 {
|
|
|
|
return fmt.Errorf(
|
|
|
|
"Expected response with length 1, got: %s", awsutil.StringValue(resp))
|
|
|
|
}
|
|
|
|
|
|
|
|
sir := *resp.SpotInstanceRequests[0]
|
|
|
|
d.SetId(*sir.SpotInstanceRequestID)
|
|
|
|
|
|
|
|
if d.Get("wait_for_fulfillment").(bool) {
|
|
|
|
spotStateConf := &resource.StateChangeConf{
|
|
|
|
// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-bid-status.html
|
|
|
|
Pending: []string{"start", "pending-evaluation", "pending-fulfillment"},
|
|
|
|
Target: "fulfilled",
|
|
|
|
Refresh: SpotInstanceStateRefreshFunc(conn, sir),
|
|
|
|
Timeout: 10 * time.Minute,
|
|
|
|
Delay: 10 * time.Second,
|
|
|
|
MinTimeout: 3 * time.Second,
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("[DEBUG] waiting for spot bid to resolve... this may take several minutes.")
|
|
|
|
_, err = spotStateConf.WaitForState()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Error while waiting for spot request (%s) to resolve: %s", awsutil.StringValue(sir), err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return resourceAwsSpotInstanceRequestUpdate(d, meta)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update spot state, etc
|
|
|
|
func resourceAwsSpotInstanceRequestRead(d *schema.ResourceData, meta interface{}) error {
|
|
|
|
conn := meta.(*AWSClient).ec2conn
|
|
|
|
|
|
|
|
req := &ec2.DescribeSpotInstanceRequestsInput{
|
|
|
|
SpotInstanceRequestIDs: []*string{aws.String(d.Id())},
|
|
|
|
}
|
|
|
|
resp, err := conn.DescribeSpotInstanceRequests(req)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
// If the spot request was not found, return nil so that we can show
|
|
|
|
// that it is gone.
|
|
|
|
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" {
|
|
|
|
d.SetId("")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Some other error, report it
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If nothing was found, then return no state
|
|
|
|
if len(resp.SpotInstanceRequests) == 0 {
|
|
|
|
d.SetId("")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
request := resp.SpotInstanceRequests[0]
|
|
|
|
|
|
|
|
// if the request is cancelled, then it is gone
|
|
|
|
if *request.State == "canceled" {
|
|
|
|
d.SetId("")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
d.Set("spot_bid_status", *request.Status.Code)
|
|
|
|
d.Set("spot_instance_id", *request.InstanceID)
|
|
|
|
d.Set("spot_request_state", *request.State)
|
|
|
|
d.Set("tags", tagsToMap(request.Tags))
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func resourceAwsSpotInstanceRequestUpdate(d *schema.ResourceData, meta interface{}) error {
|
|
|
|
conn := meta.(*AWSClient).ec2conn
|
|
|
|
|
|
|
|
d.Partial(true)
|
|
|
|
if err := setTags(conn, d); err != nil {
|
|
|
|
return err
|
|
|
|
} else {
|
|
|
|
d.SetPartial("tags")
|
|
|
|
}
|
|
|
|
|
|
|
|
d.Partial(false)
|
|
|
|
|
|
|
|
return resourceAwsInstanceRead(d, meta)
|
|
|
|
}
|
|
|
|
|
|
|
|
func resourceAwsSpotInstanceRequestDelete(d *schema.ResourceData, meta interface{}) error {
|
|
|
|
conn := meta.(*AWSClient).ec2conn
|
|
|
|
|
|
|
|
log.Printf("[INFO] Cancelling spot request: %s", d.Id())
|
|
|
|
_, err := conn.CancelSpotInstanceRequests(&ec2.CancelSpotInstanceRequestsInput{
|
|
|
|
SpotInstanceRequestIDs: []*string{aws.String(d.Id())},
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Error cancelling spot request (%s): %s", d.Id(), err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if instanceId := d.Get("spot_instance_id").(string); instanceId != "" {
|
|
|
|
log.Printf("[INFO] Terminating instance: %s", instanceId)
|
|
|
|
if err := awsTerminateInstance(conn, instanceId); err != nil {
|
|
|
|
return fmt.Errorf("Error terminating spot instance: %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SpotInstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
|
|
|
|
// an EC2 spot instance request
|
|
|
|
func SpotInstanceStateRefreshFunc(
|
|
|
|
conn *ec2.EC2, sir ec2.SpotInstanceRequest) resource.StateRefreshFunc {
|
|
|
|
|
|
|
|
return func() (interface{}, string, error) {
|
|
|
|
resp, err := conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{
|
|
|
|
SpotInstanceRequestIDs: []*string{sir.SpotInstanceRequestID},
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" {
|
|
|
|
// Set this to nil as if we didn't find anything.
|
|
|
|
resp = nil
|
|
|
|
} else {
|
|
|
|
log.Printf("Error on StateRefresh: %s", err)
|
|
|
|
return nil, "", err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp == nil || len(resp.SpotInstanceRequests) == 0 {
|
|
|
|
// Sometimes AWS just has consistency issues and doesn't see
|
|
|
|
// our request yet. Return an empty state.
|
|
|
|
return nil, "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
req := resp.SpotInstanceRequests[0]
|
|
|
|
return req, *req.Status.Code, nil
|
|
|
|
}
|
|
|
|
}
|