github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/builtin/providers/aws/resource_aws_spot_instance_request.go (about)

     1  package aws
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"time"
     7  
     8  	"github.com/aws/aws-sdk-go/aws"
     9  	"github.com/aws/aws-sdk-go/aws/awserr"
    10  	"github.com/aws/aws-sdk-go/service/ec2"
    11  	"github.com/hashicorp/terraform/helper/resource"
    12  	"github.com/hashicorp/terraform/helper/schema"
    13  )
    14  
    15  func resourceAwsSpotInstanceRequest() *schema.Resource {
    16  	return &schema.Resource{
    17  		Create: resourceAwsSpotInstanceRequestCreate,
    18  		Read:   resourceAwsSpotInstanceRequestRead,
    19  		Delete: resourceAwsSpotInstanceRequestDelete,
    20  		Update: resourceAwsSpotInstanceRequestUpdate,
    21  
    22  		Schema: func() map[string]*schema.Schema {
    23  			// The Spot Instance Request Schema is based on the AWS Instance schema.
    24  			s := resourceAwsInstance().Schema
    25  
    26  			// Everything on a spot instance is ForceNew except tags
    27  			for k, v := range s {
    28  				if k == "tags" {
    29  					continue
    30  				}
    31  				v.ForceNew = true
    32  			}
    33  
    34  			s["spot_price"] = &schema.Schema{
    35  				Type:     schema.TypeString,
    36  				Required: true,
    37  				ForceNew: true,
    38  			}
    39  			s["spot_type"] = &schema.Schema{
    40  				Type:     schema.TypeString,
    41  				Optional: true,
    42  				Default:  "persistent",
    43  			}
    44  			s["wait_for_fulfillment"] = &schema.Schema{
    45  				Type:     schema.TypeBool,
    46  				Optional: true,
    47  				Default:  false,
    48  			}
    49  			s["spot_bid_status"] = &schema.Schema{
    50  				Type:     schema.TypeString,
    51  				Computed: true,
    52  			}
    53  			s["spot_request_state"] = &schema.Schema{
    54  				Type:     schema.TypeString,
    55  				Computed: true,
    56  			}
    57  			s["spot_instance_id"] = &schema.Schema{
    58  				Type:     schema.TypeString,
    59  				Computed: true,
    60  			}
    61  			s["block_duration_minutes"] = &schema.Schema{
    62  				Type:     schema.TypeInt,
    63  				Optional: true,
    64  				ForceNew: true,
    65  			}
    66  
    67  			return s
    68  		}(),
    69  	}
    70  }
    71  
    72  func resourceAwsSpotInstanceRequestCreate(d *schema.ResourceData, meta interface{}) error {
    73  	conn := meta.(*AWSClient).ec2conn
    74  
    75  	instanceOpts, err := buildAwsInstanceOpts(d, meta)
    76  	if err != nil {
    77  		return err
    78  	}
    79  
    80  	spotOpts := &ec2.RequestSpotInstancesInput{
    81  		SpotPrice: aws.String(d.Get("spot_price").(string)),
    82  		Type:      aws.String(d.Get("spot_type").(string)),
    83  
    84  		// Though the AWS API supports creating spot instance requests for multiple
    85  		// instances, for TF purposes we fix this to one instance per request.
    86  		// Users can get equivalent behavior out of TF's "count" meta-parameter.
    87  		InstanceCount: aws.Int64(1),
    88  
    89  		LaunchSpecification: &ec2.RequestSpotLaunchSpecification{
    90  			BlockDeviceMappings: instanceOpts.BlockDeviceMappings,
    91  			EbsOptimized:        instanceOpts.EBSOptimized,
    92  			Monitoring:          instanceOpts.Monitoring,
    93  			IamInstanceProfile:  instanceOpts.IAMInstanceProfile,
    94  			ImageId:             instanceOpts.ImageID,
    95  			InstanceType:        instanceOpts.InstanceType,
    96  			KeyName:             instanceOpts.KeyName,
    97  			Placement:           instanceOpts.SpotPlacement,
    98  			SecurityGroupIds:    instanceOpts.SecurityGroupIDs,
    99  			SecurityGroups:      instanceOpts.SecurityGroups,
   100  			SubnetId:            instanceOpts.SubnetID,
   101  			UserData:            instanceOpts.UserData64,
   102  		},
   103  	}
   104  
   105  	if v, ok := d.GetOk("block_duration_minutes"); ok {
   106  		spotOpts.BlockDurationMinutes = aws.Int64(int64(v.(int)))
   107  	}
   108  
   109  	// If the instance is configured with a Network Interface (a subnet, has
   110  	// public IP, etc), then the instanceOpts.SecurityGroupIds and SubnetId will
   111  	// be nil
   112  	if len(instanceOpts.NetworkInterfaces) > 0 {
   113  		spotOpts.LaunchSpecification.SecurityGroupIds = instanceOpts.NetworkInterfaces[0].Groups
   114  		spotOpts.LaunchSpecification.SubnetId = instanceOpts.NetworkInterfaces[0].SubnetId
   115  	}
   116  
   117  	// Make the spot instance request
   118  	log.Printf("[DEBUG] Requesting spot bid opts: %s", spotOpts)
   119  
   120  	var resp *ec2.RequestSpotInstancesOutput
   121  	err = resource.Retry(15*time.Second, func() *resource.RetryError {
   122  		var err error
   123  		resp, err = conn.RequestSpotInstances(spotOpts)
   124  		// IAM instance profiles can take ~10 seconds to propagate in AWS:
   125  		// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console
   126  		if isAWSErr(err, "InvalidParameterValue", "Invalid IAM Instance Profile") {
   127  			log.Printf("[DEBUG] Invalid IAM Instance Profile referenced, retrying...")
   128  			return resource.RetryableError(err)
   129  		}
   130  		// IAM roles can also take time to propagate in AWS:
   131  		if isAWSErr(err, "InvalidParameterValue", " has no associated IAM Roles") {
   132  			log.Printf("[DEBUG] IAM Instance Profile appears to have no IAM roles, retrying...")
   133  			return resource.RetryableError(err)
   134  		}
   135  		return resource.NonRetryableError(err)
   136  	})
   137  
   138  	if err != nil {
   139  		return fmt.Errorf("Error requesting spot instances: %s", err)
   140  	}
   141  	if len(resp.SpotInstanceRequests) != 1 {
   142  		return fmt.Errorf(
   143  			"Expected response with length 1, got: %s", resp)
   144  	}
   145  
   146  	sir := *resp.SpotInstanceRequests[0]
   147  	d.SetId(*sir.SpotInstanceRequestId)
   148  
   149  	if d.Get("wait_for_fulfillment").(bool) {
   150  		spotStateConf := &resource.StateChangeConf{
   151  			// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-bid-status.html
   152  			Pending:    []string{"start", "pending-evaluation", "pending-fulfillment"},
   153  			Target:     []string{"fulfilled"},
   154  			Refresh:    SpotInstanceStateRefreshFunc(conn, sir),
   155  			Timeout:    10 * time.Minute,
   156  			Delay:      10 * time.Second,
   157  			MinTimeout: 3 * time.Second,
   158  		}
   159  
   160  		log.Printf("[DEBUG] waiting for spot bid to resolve... this may take several minutes.")
   161  		_, err = spotStateConf.WaitForState()
   162  
   163  		if err != nil {
   164  			return fmt.Errorf("Error while waiting for spot request (%s) to resolve: %s", sir, err)
   165  		}
   166  	}
   167  
   168  	return resourceAwsSpotInstanceRequestUpdate(d, meta)
   169  }
   170  
   171  // Update spot state, etc
   172  func resourceAwsSpotInstanceRequestRead(d *schema.ResourceData, meta interface{}) error {
   173  	conn := meta.(*AWSClient).ec2conn
   174  
   175  	req := &ec2.DescribeSpotInstanceRequestsInput{
   176  		SpotInstanceRequestIds: []*string{aws.String(d.Id())},
   177  	}
   178  	resp, err := conn.DescribeSpotInstanceRequests(req)
   179  
   180  	if err != nil {
   181  		// If the spot request was not found, return nil so that we can show
   182  		// that it is gone.
   183  		if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" {
   184  			d.SetId("")
   185  			return nil
   186  		}
   187  
   188  		// Some other error, report it
   189  		return err
   190  	}
   191  
   192  	// If nothing was found, then return no state
   193  	if len(resp.SpotInstanceRequests) == 0 {
   194  		d.SetId("")
   195  		return nil
   196  	}
   197  
   198  	request := resp.SpotInstanceRequests[0]
   199  
   200  	// if the request is cancelled, then it is gone
   201  	if *request.State == "cancelled" {
   202  		d.SetId("")
   203  		return nil
   204  	}
   205  
   206  	d.Set("spot_bid_status", *request.Status.Code)
   207  	// Instance ID is not set if the request is still pending
   208  	if request.InstanceId != nil {
   209  		d.Set("spot_instance_id", *request.InstanceId)
   210  		// Read the instance data, setting up connection information
   211  		if err := readInstance(d, meta); err != nil {
   212  			return fmt.Errorf("[ERR] Error reading Spot Instance Data: %s", err)
   213  		}
   214  	}
   215  
   216  	d.Set("spot_request_state", request.State)
   217  	d.Set("block_duration_minutes", request.BlockDurationMinutes)
   218  	d.Set("tags", tagsToMap(request.Tags))
   219  
   220  	return nil
   221  }
   222  
   223  func readInstance(d *schema.ResourceData, meta interface{}) error {
   224  	conn := meta.(*AWSClient).ec2conn
   225  
   226  	resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
   227  		InstanceIds: []*string{aws.String(d.Get("spot_instance_id").(string))},
   228  	})
   229  	if err != nil {
   230  		// If the instance was not found, return nil so that we can show
   231  		// that the instance is gone.
   232  		if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" {
   233  			return fmt.Errorf("no instance found")
   234  		}
   235  
   236  		// Some other error, report it
   237  		return err
   238  	}
   239  
   240  	// If nothing was found, then return no state
   241  	if len(resp.Reservations) == 0 {
   242  		return fmt.Errorf("no instances found")
   243  	}
   244  
   245  	instance := resp.Reservations[0].Instances[0]
   246  
   247  	// Set these fields for connection information
   248  	if instance != nil {
   249  		d.Set("public_dns", instance.PublicDnsName)
   250  		d.Set("public_ip", instance.PublicIpAddress)
   251  		d.Set("private_dns", instance.PrivateDnsName)
   252  		d.Set("private_ip", instance.PrivateIpAddress)
   253  
   254  		// set connection information
   255  		if instance.PublicIpAddress != nil {
   256  			d.SetConnInfo(map[string]string{
   257  				"type": "ssh",
   258  				"host": *instance.PublicIpAddress,
   259  			})
   260  		} else if instance.PrivateIpAddress != nil {
   261  			d.SetConnInfo(map[string]string{
   262  				"type": "ssh",
   263  				"host": *instance.PrivateIpAddress,
   264  			})
   265  		}
   266  		if err := readBlockDevices(d, instance, conn); err != nil {
   267  			return err
   268  		}
   269  	}
   270  
   271  	return nil
   272  }
   273  
   274  func resourceAwsSpotInstanceRequestUpdate(d *schema.ResourceData, meta interface{}) error {
   275  	conn := meta.(*AWSClient).ec2conn
   276  
   277  	d.Partial(true)
   278  	if err := setTags(conn, d); err != nil {
   279  		return err
   280  	} else {
   281  		d.SetPartial("tags")
   282  	}
   283  
   284  	d.Partial(false)
   285  
   286  	return resourceAwsSpotInstanceRequestRead(d, meta)
   287  }
   288  
   289  func resourceAwsSpotInstanceRequestDelete(d *schema.ResourceData, meta interface{}) error {
   290  	conn := meta.(*AWSClient).ec2conn
   291  
   292  	log.Printf("[INFO] Cancelling spot request: %s", d.Id())
   293  	_, err := conn.CancelSpotInstanceRequests(&ec2.CancelSpotInstanceRequestsInput{
   294  		SpotInstanceRequestIds: []*string{aws.String(d.Id())},
   295  	})
   296  
   297  	if err != nil {
   298  		return fmt.Errorf("Error cancelling spot request (%s): %s", d.Id(), err)
   299  	}
   300  
   301  	if instanceId := d.Get("spot_instance_id").(string); instanceId != "" {
   302  		log.Printf("[INFO] Terminating instance: %s", instanceId)
   303  		if err := awsTerminateInstance(conn, instanceId); err != nil {
   304  			return fmt.Errorf("Error terminating spot instance: %s", err)
   305  		}
   306  	}
   307  
   308  	return nil
   309  }
   310  
   311  // SpotInstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
   312  // an EC2 spot instance request
   313  func SpotInstanceStateRefreshFunc(
   314  	conn *ec2.EC2, sir ec2.SpotInstanceRequest) resource.StateRefreshFunc {
   315  
   316  	return func() (interface{}, string, error) {
   317  		resp, err := conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{
   318  			SpotInstanceRequestIds: []*string{sir.SpotInstanceRequestId},
   319  		})
   320  
   321  		if err != nil {
   322  			if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" {
   323  				// Set this to nil as if we didn't find anything.
   324  				resp = nil
   325  			} else {
   326  				log.Printf("Error on StateRefresh: %s", err)
   327  				return nil, "", err
   328  			}
   329  		}
   330  
   331  		if resp == nil || len(resp.SpotInstanceRequests) == 0 {
   332  			// Sometimes AWS just has consistency issues and doesn't see
   333  			// our request yet. Return an empty state.
   334  			return nil, "", nil
   335  		}
   336  
   337  		req := resp.SpotInstanceRequests[0]
   338  		return req, *req.Status.Code, nil
   339  	}
   340  }