diff --git a/aws/resource_aws_instance.go b/aws/resource_aws_instance.go index 225db4bac6d..40e1fe673c6 100644 --- a/aws/resource_aws_instance.go +++ b/aws/resource_aws_instance.go @@ -454,6 +454,49 @@ func resourceAwsInstance() *schema.Resource { }, }, }, + + "instance_market_options": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "market_type": { + Type: schema.TypeString, + Optional: true, + }, + "spot_options": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "block_duration_minutes": { + Type: schema.TypeInt, + Optional: true, + }, + "instance_interruption_behavior": { + Type: schema.TypeString, + Optional: true, + }, + "max_price": { + Type: schema.TypeString, + Optional: true, + }, + "spot_instance_type": { + Type: schema.TypeString, + Optional: true, + }, + "valid_until": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, }, } } @@ -497,6 +540,7 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { SubnetId: instanceOpts.SubnetID, UserData: instanceOpts.UserData64, CreditSpecification: instanceOpts.CreditSpecification, + InstanceMarketOptions: instanceOpts.InstanceMarketOptions, } _, ipv6CountOk := d.GetOk("ipv6_address_count") @@ -772,6 +816,9 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { if _, ok := d.GetOk("ephemeral_block_device"); !ok { d.Set("ephemeral_block_device", []interface{}{}) } + if err := readSpotRequest(d, instance, conn); err != nil { + return err + } // Instance attributes { @@ -1681,6 +1728,7 @@ type awsInstanceOpts struct { SubnetID *string UserData64 *string CreditSpecification *ec2.CreditSpecificationRequest + InstanceMarketOptions *ec2.InstanceMarketOptionsRequest } func buildAwsInstanceOpts( @@ -1814,6 +1862,40 @@ func buildAwsInstanceOpts( if len(blockDevices) > 0 { opts.BlockDeviceMappings = blockDevices } + if v, ok := d.GetOk("instance_market_options"); ok { + imo := v.(*schema.Set).List() + + if len(imo) > 0 { + imoData := imo[0].(map[string]interface{}) + spotOptions := &ec2.SpotMarketOptions{} + + if v, ok := imoData["spot_options"]; ok { + vL := v.(*schema.Set).List() + for _, v := range vL { + so := v.(map[string]interface{}) + spotOptions.BlockDurationMinutes = aws.Int64(int64(so["block_duration_minutes"].(int))) + spotOptions.InstanceInterruptionBehavior = aws.String(so["instance_interruption_behavior"].(string)) + spotOptions.MaxPrice = aws.String(so["max_price"].(string)) + spotOptions.SpotInstanceType = aws.String(so["spot_instance_type"].(string)) + + if so["valid_until"] != "" { + t, err := time.Parse(time.RFC3339, so["valid_until"].(string)) + if err != nil { + return nil, fmt.Errorf("Error Parsing Launch Template Spot Options valid until: %s", err.Error()) + } + spotOptions.ValidUntil = aws.Time(t) + } + } + } + + instanceMarketOptions := &ec2.InstanceMarketOptionsRequest{ + MarketType: aws.String(imoData["market_type"].(string)), + SpotOptions: spotOptions, + } + + opts.InstanceMarketOptions = instanceMarketOptions + } + } return opts, nil } @@ -1908,3 +1990,41 @@ func getCreditSpecifications(conn *ec2.EC2, instanceId string) ([]map[string]int return creditSpecifications, nil } + +func readSpotRequest(d *schema.ResourceData, instance *ec2.Instance, conn *ec2.EC2) error { + marketOptions := make([]map[string]interface{}, 0) + dsir, err := readSpotRequestFromInstance(instance, conn) + if err != nil { + return err + } + if len(dsir) > 0 { + marketOption := make(map[string]interface{}) + marketOption["market_type"] = "spot" + marketOption["spot_options"] = dsir + marketOptions = append(marketOptions, marketOption) + } + d.Set("instance_market_options", marketOptions) + return nil +} + +func readSpotRequestFromInstance(instance *ec2.Instance, conn *ec2.EC2) ([]interface{}, error) { + spotRequests := make([]interface{}, 0) + if instance.SpotInstanceRequestId != nil { + spotInstanceRequest, err := conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{ + SpotInstanceRequestIds: []*string{instance.SpotInstanceRequestId}, + }) + if err != nil { + return nil, err + } + for _, sr := range spotInstanceRequest.SpotInstanceRequests { + spotRequest := make(map[string]interface{}) + spotRequest["block_duration_minutes"] = *sr.BlockDurationMinutes + spotRequest["instance_interruption_behavior"] = *sr.InstanceInterruptionBehavior + spotRequest["max_price"] = *sr.SpotPrice + spotRequest["spot_instance_type"] = *sr.Type + spotRequest["valid_until"] = aws.TimeValue(sr.ValidUntil).Format(time.RFC3339) + spotRequests = append(spotRequests, spotRequest) + } + } + return spotRequests, nil +} diff --git a/aws/resource_aws_instance_test.go b/aws/resource_aws_instance_test.go index a9356dc5f9a..6079e156b18 100644 --- a/aws/resource_aws_instance_test.go +++ b/aws/resource_aws_instance_test.go @@ -1520,6 +1520,26 @@ func TestAccAWSInstance_creditSpecification_removalReturnsStandard(t *testing.T) }) } +func TestAccAWSInstance_instanceMarketOptions(t *testing.T) { + var instance ec2.Instance + resName := "aws_instance.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccInstanceConfig_instanceMarketOptions, + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceExists(resName, &instance), + resource.TestCheckResourceAttr(resName, "instance_market_options.#", "1"), + ), + }, + }, + }) +} + func testAccCheckInstanceNotRecreated(t *testing.T, before, after *ec2.Instance) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -3168,3 +3188,36 @@ resource "aws_instance" "foo" { } `, rInt) } + +const testAccInstanceConfig_instanceMarketOptions = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" + tags { + Name = "terraform-testacc-instance-market-options" + } +} + +resource "aws_subnet" "foo" { + cidr_block = "10.1.1.0/24" + vpc_id = "${aws_vpc.foo.id}" +} + +resource "aws_instance" "foo" { + ami = "ami-22b9a343" # us-west-2 + instance_type = "t2.medium" + subnet_id = "${aws_subnet.foo.id}" + + instance_market_options { + market_type = "spot" + spot_options { + spot_instance_type = "one-time" + max_price = "0.02" + block_duration_minutes = "60" + instance_interruption_behavior = "terminate" + } + } + tags { + Name = "tf-acctest-instance-market-options" + } +} +` diff --git a/website/docs/r/instance.html.markdown b/website/docs/r/instance.html.markdown index e762b37c2f1..2649f368e6e 100644 --- a/website/docs/r/instance.html.markdown +++ b/website/docs/r/instance.html.markdown @@ -91,7 +91,9 @@ instances. See [Shutdown Behavior](https://docs.aws.amazon.com/AWSEC2/latest/Use "Instance Store") volumes on the instance. See [Block Devices](#block-devices) below for details. * `network_interface` - (Optional) Customize network interfaces to be attached at instance boot time. See [Network Interfaces](#network-interfaces) below for more details. * `credit_specification` - (Optional) Customize the credit specification of the instance. See [Credit Specification](#credit-specification) below for more details. - +* `instance_market_options` - (Optional) The market (purchasing) option for the instance. See [Market Options](#market-options) + below for details. + ### Timeouts The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: @@ -185,6 +187,24 @@ The `credit_specification` block supports the following: * `cpu_credits` - (Optional) The credit option for CPU usage. +### Market Options + +The market (purchasing) option for the instance. + +The `instance_market_options` block supports the following: + +* `market_type` - The market type. Can be `spot`. +* `spot_options` - The options for [Spot Instance](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-spot-instances.html) + +The `spot_options` block supports the following: + +* `block_duration_minutes` - The required duration in minutes. This value must be a multiple of 60. +* `instance_interruption_behavior` - The behavior when a Spot Instance is interrupted. Can be `hibernate`, + `stop`, or `terminate`. +* `max_price` - The maximum hourly price you're willing to pay for the Spot Instances. +* `spot_instance_type` - The Spot Instance request type. Can be `one-time`, or `persistent`. +* `valid_until` - The end date of the request. + ### Example ```hcl