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