provider/aws: AWS SpotFleet Requests now works with Subnets and AZs (#8320)

* provider/aws: Change Spot Fleet Request to allow a combination of
subnet_id and availability_zone

Also added a complete set of tests that reflect all of the use cases
that Amazon document
http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-fleet-examples.html

It is important to note there that Terraform will be suggesting that
users create multiple launch configurations rather than AWS's version of
combing values into CSV based parameters. This will ensure that we are
able to enforce the correct state

Also note that `associate_public_ip_address` now defaults to `false` - a migration has been
included in this PR to migration users of this functionality. This needs
to be noted in the changelog. The last part of changing functionality
here is waiting for the state of the request to become `active`. Before
we get to this state, we cannot guarantee that Amazon have accepted the
request or it could have failed validation.

```
% make testacc TEST=./builtin/providers/aws
% TESTARGS='-run=TestAccAWSSpotFleetRequest_'
% 2 ↵
==> Checking that code complies with gofmt requirements...
/Users/stacko/Code/go/bin/stringer
go generate $(go list ./... | grep -v /terraform/vendor/)
2016/08/22 15:44:21 Generated command/internal_plugin_list.go
TF_ACC=1 go test ./builtin/providers/aws -v
-run=TestAccAWSSpotFleetRequest_ -timeout 120m
=== RUN   TestAccAWSSpotFleetRequest_changePriceForcesNewRequest
--- PASS: TestAccAWSSpotFleetRequest_changePriceForcesNewRequest (133.90s)
=== RUN   TestAccAWSSpotFleetRequest_lowestPriceAzOrSubnetInRegion
--- PASS: TestAccAWSSpotFleetRequest_lowestPriceAzOrSubnetInRegion (76.67s)
=== RUN   TestAccAWSSpotFleetRequest_lowestPriceAzInGivenList
--- PASS: TestAccAWSSpotFleetRequest_lowestPriceAzInGivenList (75.22s)
=== RUN   TestAccAWSSpotFleetRequest_lowestPriceSubnetInGivenList
--- PASS: TestAccAWSSpotFleetRequest_lowestPriceSubnetInGivenList (96.95s)
=== RUN   TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameAz
--- PASS: TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameAz (74.44s)
=== RUN   TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameSubnet
--- PASS: TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameSubnet (97.82s)
=== RUN   TestAccAWSSpotFleetRequest_overriddingSpotPrice
--- PASS: TestAccAWSSpotFleetRequest_overriddingSpotPrice (76.22s)
=== RUN   TestAccAWSSpotFleetRequest_diversifiedAllocation
--- PASS: TestAccAWSSpotFleetRequest_diversifiedAllocation (79.81s)
=== RUN   TestAccAWSSpotFleetRequest_withWeightedCapacity
--- PASS: TestAccAWSSpotFleetRequest_withWeightedCapacity (77.15s)
=== RUN   TestAccAWSSpotFleetRequest_CannotUseEmptyKeyName
--- PASS: TestAccAWSSpotFleetRequest_CannotUseEmptyKeyName (0.00s)
PASS
ok      github.com/hashicorp/terraform/builtin/providers/aws    788.184s
```

* Update resource_aws_spot_fleet_request.go
This commit is contained in:
Paul Stack 2016-08-24 11:08:46 +01:00 committed by GitHub
parent 6095c7465b
commit e524603d3f
5 changed files with 804 additions and 131 deletions

View File

@ -25,6 +25,9 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
Delete: resourceAwsSpotFleetRequestDelete,
Update: resourceAwsSpotFleetRequestUpdate,
SchemaVersion: 1,
MigrateState: resourceAwsSpotFleetRequestMigrateState,
Schema: map[string]*schema.Schema{
"iam_fleet_role": &schema.Schema{
Type: schema.TypeString,
@ -49,7 +52,7 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
"associate_public_ip_address": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
Default: false,
},
"ebs_block_device": &schema.Schema{
Type: schema.TypeSet,
@ -192,7 +195,6 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
Type: schema.TypeBool,
Optional: true,
},
// "network_interface_set"
"placement_group": &schema.Schema{
Type: schema.TypeString,
Optional: true,
@ -204,12 +206,6 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
Optional: true,
ForceNew: true,
},
"subnet_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"user_data": &schema.Schema{
Type: schema.TypeString,
Optional: true,
@ -229,9 +225,16 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
Optional: true,
ForceNew: true,
},
"subnet_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"availability_zone": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
@ -291,19 +294,16 @@ func resourceAwsSpotFleetRequest() *schema.Resource {
func buildSpotFleetLaunchSpecification(d map[string]interface{}, meta interface{}) (*ec2.SpotFleetLaunchSpecification, error) {
conn := meta.(*AWSClient).ec2conn
_, hasSubnet := d["subnet_id"]
_, hasAZ := d["availability_zone"]
if !hasAZ && !hasSubnet {
return nil, fmt.Errorf("LaunchSpecification must include a subnet_id or an availability_zone")
}
opts := &ec2.SpotFleetLaunchSpecification{
ImageId: aws.String(d["ami"].(string)),
InstanceType: aws.String(d["instance_type"].(string)),
SpotPrice: aws.String(d["spot_price"].(string)),
Placement: &ec2.SpotPlacement{
AvailabilityZone: aws.String(d["availability_zone"].(string)),
},
}
if v, ok := d["availability_zone"]; ok {
opts.Placement = &ec2.SpotPlacement{
AvailabilityZone: aws.String(v.(string)),
}
}
if v, ok := d["ebs_optimized"]; ok {
@ -327,70 +327,6 @@ func buildSpotFleetLaunchSpecification(d map[string]interface{}, meta interface{
base64.StdEncoding.EncodeToString([]byte(v.(string))))
}
// check for non-default Subnet, and cast it to a String
subnet, hasSubnet := d["subnet_id"]
subnetID := subnet.(string)
var associatePublicIPAddress bool
if v, ok := d["associate_public_ip_address"]; ok {
associatePublicIPAddress = v.(bool)
}
var groups []*string
if v, ok := d["security_groups"]; ok {
// Security group names.
// For a nondefault VPC, you must use security group IDs instead.
// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RunInstances.html
sgs := v.(*schema.Set).List()
if len(sgs) > 0 && hasSubnet {
log.Printf("[WARN] Deprecated. Attempting to use 'security_groups' within a VPC instance. Use 'vpc_security_group_ids' instead.")
}
for _, v := range sgs {
str := v.(string)
groups = append(groups, aws.String(str))
}
}
if hasSubnet && associatePublicIPAddress {
// If we have a non-default VPC / Subnet specified, we can flag
// AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided.
// You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise
// you get: Network interfaces and an instance-level subnet ID may not be specified on the same request
// You also need to attach Security Groups to the NetworkInterface instead of the instance,
// to avoid: Network interfaces and an instance-level security groups may not be specified on
// the same request
ni := &ec2.InstanceNetworkInterfaceSpecification{
AssociatePublicIpAddress: aws.Bool(associatePublicIPAddress),
DeviceIndex: aws.Int64(int64(0)),
SubnetId: aws.String(subnetID),
Groups: groups,
}
if v, ok := d["private_ip"]; ok {
ni.PrivateIpAddress = aws.String(v.(string))
}
if v := d["vpc_security_group_ids"].(*schema.Set); v.Len() > 0 {
for _, v := range v.List() {
ni.Groups = append(ni.Groups, aws.String(v.(string)))
}
}
opts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni}
} else {
if subnetID != "" {
opts.SubnetId = aws.String(subnetID)
}
if v, ok := d["vpc_security_group_ids"]; ok {
if s := v.(*schema.Set); s.Len() > 0 {
for _, v := range s.List() {
opts.SecurityGroups = append(opts.SecurityGroups, &ec2.GroupIdentifier{GroupId: aws.String(v.(string))})
}
}
}
}
if v, ok := d["key_name"]; ok {
opts.KeyName = aws.String(v.(string))
}
@ -403,6 +339,51 @@ func buildSpotFleetLaunchSpecification(d map[string]interface{}, meta interface{
opts.WeightedCapacity = aws.Float64(wc)
}
var groups []*string
if v, ok := d["security_groups"]; ok {
sgs := v.(*schema.Set).List()
for _, v := range sgs {
str := v.(string)
groups = append(groups, aws.String(str))
}
}
var groupIds []*string
if v, ok := d["vpc_security_group_ids"]; ok {
if s := v.(*schema.Set); s.Len() > 0 {
for _, v := range s.List() {
opts.SecurityGroups = append(opts.SecurityGroups, &ec2.GroupIdentifier{GroupId: aws.String(v.(string))})
groupIds = append(groupIds, aws.String(v.(string)))
}
}
}
subnetId, hasSubnetId := d["subnet_id"]
if hasSubnetId {
opts.SubnetId = aws.String(subnetId.(string))
}
associatePublicIpAddress, hasPublicIpAddress := d["associate_public_ip_address"]
if hasPublicIpAddress && associatePublicIpAddress.(bool) == true && hasSubnetId {
// If we have a non-default VPC / Subnet specified, we can flag
// AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided.
// You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise
// you get: Network interfaces and an instance-level subnet ID may not be specified on the same request
// You also need to attach Security Groups to the NetworkInterface instead of the instance,
// to avoid: Network interfaces and an instance-level security groups may not be specified on
// the same request
ni := &ec2.InstanceNetworkInterfaceSpecification{
AssociatePublicIpAddress: aws.Bool(true),
DeviceIndex: aws.Int64(int64(0)),
SubnetId: aws.String(subnetId.(string)),
Groups: groupIds,
}
opts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni}
opts.SubnetId = aws.String("")
}
blockDevices, err := readSpotFleetBlockDeviceMappingsFromConfig(d, conn)
if err != nil {
return nil, err
@ -617,9 +598,52 @@ func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{})
d.SetId(*resp.SpotFleetRequestId)
log.Printf("[INFO] Spot Fleet Request ID: %s", d.Id())
log.Println("[INFO] Waiting for Spot Fleet Request to be active")
stateConf := &resource.StateChangeConf{
Pending: []string{"submitted"},
Target: []string{"active"},
Refresh: resourceAwsSpotFleetRequestStateRefreshFunc(d, meta),
Timeout: 10 * time.Minute,
MinTimeout: 10 * time.Second,
Delay: 30 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return err
}
return resourceAwsSpotFleetRequestRead(d, meta)
}
func resourceAwsSpotFleetRequestStateRefreshFunc(d *schema.ResourceData, meta interface{}) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
conn := meta.(*AWSClient).ec2conn
req := &ec2.DescribeSpotFleetRequestsInput{
SpotFleetRequestIds: []*string{aws.String(d.Id())},
}
resp, err := conn.DescribeSpotFleetRequests(req)
if err != nil {
log.Printf("Error on retrieving Spot Fleet Request when waiting: %s", err)
return nil, "", nil
}
if resp == nil {
return nil, "", nil
}
if len(resp.SpotFleetRequestConfigs) == 0 {
return nil, "", nil
}
spotFleetRequest := resp.SpotFleetRequestConfigs[0]
return spotFleetRequest, *spotFleetRequest.SpotFleetRequestState, nil
}
}
func resourceAwsSpotFleetRequestRead(d *schema.ResourceData, meta interface{}) error {
// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSpotFleetRequests.html
conn := meta.(*AWSClient).ec2conn
@ -773,7 +797,7 @@ func launchSpecToMap(
}
if l.WeightedCapacity != nil {
m["weighted_capacity"] = fmt.Sprintf("%.3f", aws.Float64Value(l.WeightedCapacity))
m["weighted_capacity"] = strconv.FormatFloat(*l.WeightedCapacity, 'f', 0, 64)
}
// m["security_groups"] = securityGroupsToSet(l.SecutiryGroups)
@ -941,9 +965,10 @@ func hashLaunchSpecification(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["ami"].(string)))
if m["availability_zone"] != nil && m["availability_zone"] != "" {
if m["availability_zone"] != "" {
buf.WriteString(fmt.Sprintf("%s-", m["availability_zone"].(string)))
} else if m["subnet_id"] != nil && m["subnet_id"] != "" {
}
if m["subnet_id"] != "" {
buf.WriteString(fmt.Sprintf("%s-", m["subnet_id"].(string)))
}
buf.WriteString(fmt.Sprintf("%s-", m["instance_type"].(string)))

View File

@ -0,0 +1,33 @@
package aws
import (
"fmt"
"log"
"github.com/hashicorp/terraform/terraform"
)
func resourceAwsSpotFleetRequestMigrateState(
v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
switch v {
case 0:
log.Println("[INFO] Found AWS Spot Fleet Request State v0; migrating to v1")
return migrateSpotFleetRequestV0toV1(is)
default:
return is, fmt.Errorf("Unexpected schema version: %d", v)
}
}
func migrateSpotFleetRequestV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) {
if is.Empty() {
log.Println("[DEBUG] Empty Spot Fleet Request State; nothing to migrate.")
return is, nil
}
log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes)
is.Attributes["associate_public_ip_address"] = "false"
log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes)
return is, nil
}

View File

@ -0,0 +1,43 @@
package aws
import (
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestAWSSpotFleetRequestMigrateState(t *testing.T) {
cases := map[string]struct {
StateVersion int
ID string
Attributes map[string]string
Expected string
Meta interface{}
}{
"v0_1": {
StateVersion: 0,
ID: "some_id",
Attributes: map[string]string{
"associate_public_ip_address": "true",
},
Expected: "false",
},
}
for tn, tc := range cases {
is := &terraform.InstanceState{
ID: tc.ID,
Attributes: tc.Attributes,
}
is, err := resourceAwsSpotFleetRequestMigrateState(
tc.StateVersion, is, tc.Meta)
if err != nil {
t.Fatalf("bad: %s, err: %#v", tn, err)
}
if is.Attributes["associate_public_ip_address"] != tc.Expected {
t.Fatalf("bad Spot Fleet Request Migrate: %s\n\n expected: %s", is.Attributes["associate_public_ip_address"], tc.Expected)
}
}
}

View File

@ -3,7 +3,6 @@ package aws
import (
"encoding/base64"
"fmt"
"regexp"
"testing"
"github.com/aws/aws-sdk-go/aws"
@ -12,7 +11,46 @@ import (
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSSpotFleetRequest_basic(t *testing.T) {
func TestAccAWSSpotFleetRequest_changePriceForcesNewRequest(t *testing.T) {
var before, after ec2.SpotFleetRequestConfig
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSpotFleetRequestConfig,
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSSpotFleetRequestExists(
"aws_spot_fleet_request.foo", &before),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_request_state", "active"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_price", "0.005"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.#", "1"),
),
},
resource.TestStep{
Config: testAccAWSSpotFleetRequestConfigChangeSpotBidPrice,
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSSpotFleetRequestExists(
"aws_spot_fleet_request.foo", &after),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_request_state", "active"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.#", "1"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_price", "0.01"),
testAccCheckAWSSpotFleetRequestConfigRecreated(t, &before, &after),
),
},
},
})
}
func TestAccAWSSpotFleetRequest_lowestPriceAzOrSubnetInRegion(t *testing.T) {
var sfr ec2.SpotFleetRequestConfig
resource.Test(t, resource.TestCase{
@ -22,33 +60,20 @@ func TestAccAWSSpotFleetRequest_basic(t *testing.T) {
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSpotFleetRequestConfig,
Check: resource.ComposeTestCheckFunc(
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSSpotFleetRequestExists(
"aws_spot_fleet_request.foo", &sfr),
testAccCheckAWSSpotFleetRequestAttributes(&sfr),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_request_state", "active"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.#", "1"),
),
},
},
})
}
func TestAccAWSSpotFleetRequest_brokenLaunchSpecification(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSpotFleetRequestConfigBroken,
ExpectError: regexp.MustCompile("LaunchSpecification must include a subnet_id or an availability_zone"),
},
},
})
}
func TestAccAWSSpotFleetRequest_launchConfiguration(t *testing.T) {
func TestAccAWSSpotFleetRequest_lowestPriceAzInGivenList(t *testing.T) {
var sfr ec2.SpotFleetRequestConfig
resource.Test(t, resource.TestCase{
@ -57,13 +82,184 @@ func TestAccAWSSpotFleetRequest_launchConfiguration(t *testing.T) {
CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSpotFleetRequestWithAdvancedLaunchSpecConfig,
Check: resource.ComposeTestCheckFunc(
Config: testAccAWSSpotFleetRequestConfigWithAzs,
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSSpotFleetRequestExists(
"aws_spot_fleet_request.foo", &sfr),
testAccCheckAWSSpotFleetRequest_LaunchSpecAttributes(&sfr),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_request_state", "active"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.#", "2"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.1590006269.availability_zone", "us-west-2a"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.3809475891.availability_zone", "us-west-2b"),
),
},
},
})
}
func TestAccAWSSpotFleetRequest_lowestPriceSubnetInGivenList(t *testing.T) {
var sfr ec2.SpotFleetRequestConfig
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSpotFleetRequestConfigWithSubnet,
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSSpotFleetRequestExists(
"aws_spot_fleet_request.foo", &sfr),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_request_state", "active"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.#", "2"),
),
},
},
})
}
func TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameAz(t *testing.T) {
var sfr ec2.SpotFleetRequestConfig
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSpotFleetRequestConfigMultipleInstanceTypesinSameAz,
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSSpotFleetRequestExists(
"aws_spot_fleet_request.foo", &sfr),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_request_state", "active"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.#", "2"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.1590006269.instance_type", "m1.small"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.1590006269.availability_zone", "us-west-2a"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.3079734941.instance_type", "m3.large"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.3079734941.availability_zone", "us-west-2a"),
),
},
},
})
}
func TestAccAWSSpotFleetRequest_multipleInstanceTypesInSameSubnet(t *testing.T) {
var sfr ec2.SpotFleetRequestConfig
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSpotFleetRequestConfigMultipleInstanceTypesinSameSubnet,
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSSpotFleetRequestExists(
"aws_spot_fleet_request.foo", &sfr),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_request_state", "active"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.#", "2"),
),
},
},
})
}
func TestAccAWSSpotFleetRequest_overriddingSpotPrice(t *testing.T) {
var sfr ec2.SpotFleetRequestConfig
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSpotFleetRequestConfigOverridingSpotPrice,
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSSpotFleetRequestExists(
"aws_spot_fleet_request.foo", &sfr),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_request_state", "active"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_price", "0.005"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.#", "2"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.522395050.spot_price", "0.01"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.522395050.instance_type", "m3.large"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.1590006269.spot_price", ""), //there will not be a value here since it's not overriding
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.1590006269.instance_type", "m1.small"),
),
},
},
})
}
func TestAccAWSSpotFleetRequest_diversifiedAllocation(t *testing.T) {
var sfr ec2.SpotFleetRequestConfig
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSpotFleetRequestConfigDiversifiedAllocation,
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSSpotFleetRequestExists(
"aws_spot_fleet_request.foo", &sfr),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_request_state", "active"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.#", "3"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "allocation_strategy", "diversified"),
),
},
},
})
}
func TestAccAWSSpotFleetRequest_withWeightedCapacity(t *testing.T) {
var sfr ec2.SpotFleetRequestConfig
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSpotFleetRequestDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSpotFleetRequestConfigWithWeightedCapacity,
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSSpotFleetRequestExists(
"aws_spot_fleet_request.foo", &sfr),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_request_state", "active"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.#", "2"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.2325690000.weighted_capacity", "3"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.2325690000.instance_type", "r3.large"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.3079734941.weighted_capacity", "6"),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "launch_specification.3079734941.instance_type", "m3.large"),
),
},
},
@ -77,6 +273,16 @@ func TestAccAWSSpotFleetRequest_CannotUseEmptyKeyName(t *testing.T) {
}
}
func testAccCheckAWSSpotFleetRequestConfigRecreated(t *testing.T,
before, after *ec2.SpotFleetRequestConfig) resource.TestCheckFunc {
return func(s *terraform.State) error {
if before.SpotFleetRequestId == after.SpotFleetRequestId {
t.Fatalf("Expected change of Spot Fleet Request IDs, but both were %v", before.SpotFleetRequestId)
}
return nil
}
}
func testAccCheckAWSSpotFleetRequestExists(
n string, sfr *ec2.SpotFleetRequestConfig) resource.TestCheckFunc {
return func(s *terraform.State) error {
@ -110,19 +316,6 @@ func testAccCheckAWSSpotFleetRequestExists(
}
}
func testAccCheckAWSSpotFleetRequestAttributes(
sfr *ec2.SpotFleetRequestConfig) resource.TestCheckFunc {
return func(s *terraform.State) error {
if *sfr.SpotFleetRequestConfig.SpotPrice != "0.005" {
return fmt.Errorf("Unexpected spot price: %s", *sfr.SpotFleetRequestConfig.SpotPrice)
}
if *sfr.SpotFleetRequestState != "active" {
return fmt.Errorf("Unexpected request state: %s", *sfr.SpotFleetRequestState)
}
return nil
}
}
func testAccCheckAWSSpotFleetRequest_LaunchSpecAttributes(
sfr *ec2.SpotFleetRequestConfig) resource.TestCheckFunc {
return func(s *terraform.State) error {
@ -213,17 +406,63 @@ resource "aws_spot_fleet_request" "foo" {
spot_price = "0.005"
target_capacity = 2
valid_until = "2019-11-04T20:44:20Z"
terminate_instances_with_expiration = true
launch_specification {
instance_type = "m1.small"
ami = "ami-d06a90b0"
key_name = "${aws_key_pair.debugging.key_name}"
availability_zone = "us-west-2a"
}
depends_on = ["aws_iam_policy_attachment.test-attach"]
}
`
const testAccAWSSpotFleetRequestConfigBroken = `
const testAccAWSSpotFleetRequestConfigChangeSpotBidPrice = `
resource "aws_key_pair" "debugging" {
key_name = "tmp-key"
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 phodgson@thoughtworks.com"
}
resource "aws_iam_policy_attachment" "test-attach" {
name = "test-attachment"
roles = ["${aws_iam_role.test-role.name}"]
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetRole"
}
resource "aws_iam_role" "test-role" {
name = "test-role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "spotfleet.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_spot_fleet_request" "foo" {
iam_fleet_role = "${aws_iam_role.test-role.arn}"
spot_price = "0.01"
target_capacity = 2
valid_until = "2019-11-04T20:44:20Z"
terminate_instances_with_expiration = true
launch_specification {
instance_type = "m1.small"
ami = "ami-d06a90b0"
key_name = "${aws_key_pair.debugging.key_name}"
}
depends_on = ["aws_iam_policy_attachment.test-attach"]
}
`
const testAccAWSSpotFleetRequestConfigWithAzs = `
resource "aws_key_pair" "debugging" {
key_name = "tmp-key"
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 phodgson@thoughtworks.com"
@ -259,16 +498,93 @@ resource "aws_spot_fleet_request" "foo" {
spot_price = "0.005"
target_capacity = 2
valid_until = "2019-11-04T20:44:20Z"
terminate_instances_with_expiration = true
launch_specification {
instance_type = "m1.small"
ami = "ami-d06a90b0"
key_name = "${aws_key_pair.debugging.key_name}"
availability_zone = "us-west-2a"
}
launch_specification {
instance_type = "m1.small"
ami = "ami-d06a90b0"
key_name = "${aws_key_pair.debugging.key_name}"
availability_zone = "us-west-2b"
}
depends_on = ["aws_iam_policy_attachment.test-attach"]
}
`
const testAccAWSSpotFleetRequestWithAdvancedLaunchSpecConfig = `
const testAccAWSSpotFleetRequestConfigWithSubnet = `
resource "aws_key_pair" "debugging" {
key_name = "tmp-key"
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 phodgson@thoughtworks.com"
}
resource "aws_iam_policy_attachment" "test-attach" {
name = "test-attachment"
roles = ["${aws_iam_role.test-role.name}"]
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetRole"
}
resource "aws_iam_role" "test-role" {
name = "test-role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "spotfleet.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
}
resource "aws_subnet" "foo" {
cidr_block = "10.1.1.0/24"
vpc_id = "${aws_vpc.foo.id}"
availability_zone = "us-west-2a"
}
resource "aws_subnet" "bar" {
cidr_block = "10.1.20.0/24"
vpc_id = "${aws_vpc.foo.id}"
availability_zone = "us-west-2b"
}
resource "aws_spot_fleet_request" "foo" {
iam_fleet_role = "${aws_iam_role.test-role.arn}"
spot_price = "0.005"
target_capacity = 4
valid_until = "2019-11-04T20:44:20Z"
terminate_instances_with_expiration = true
launch_specification {
instance_type = "m3.large"
ami = "ami-d0f506b0"
key_name = "${aws_key_pair.debugging.key_name}"
subnet_id = "${aws_subnet.foo.id}"
}
launch_specification {
instance_type = "m3.large"
ami = "ami-d0f506b0"
key_name = "${aws_key_pair.debugging.key_name}"
subnet_id = "${aws_subnet.bar.id}"
}
depends_on = ["aws_iam_policy_attachment.test-attach"]
}
`
const testAccAWSSpotFleetRequestConfigMultipleInstanceTypesinSameAz = `
resource "aws_key_pair" "debugging" {
key_name = "tmp-key"
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 phodgson@thoughtworks.com"
@ -302,21 +618,252 @@ EOF
resource "aws_spot_fleet_request" "foo" {
iam_fleet_role = "${aws_iam_role.test-role.arn}"
spot_price = "0.005"
target_capacity = 4
target_capacity = 2
valid_until = "2019-11-04T20:44:20Z"
allocation_strategy = "diversified"
terminate_instances_with_expiration = true
launch_specification {
instance_type = "m1.small"
ami = "ami-d06a90b0"
key_name = "${aws_key_pair.debugging.key_name}"
availability_zone = "us-west-2a"
spot_price = "0.01"
weighted_capacity = 2
user_data = "hello-world"
root_block_device {
volume_size = "300"
volume_type = "gp2"
}
launch_specification {
instance_type = "m3.large"
ami = "ami-d06a90b0"
key_name = "${aws_key_pair.debugging.key_name}"
availability_zone = "us-west-2a"
}
depends_on = ["aws_iam_policy_attachment.test-attach"]
}
`
const testAccAWSSpotFleetRequestConfigMultipleInstanceTypesinSameSubnet = `
resource "aws_key_pair" "debugging" {
key_name = "tmp-key"
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 phodgson@thoughtworks.com"
}
resource "aws_iam_policy_attachment" "test-attach" {
name = "test-attachment"
roles = ["${aws_iam_role.test-role.name}"]
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetRole"
}
resource "aws_iam_role" "test-role" {
name = "test-role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "spotfleet.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
}
resource "aws_subnet" "foo" {
cidr_block = "10.1.1.0/24"
vpc_id = "${aws_vpc.foo.id}"
availability_zone = "us-west-2a"
}
resource "aws_spot_fleet_request" "foo" {
iam_fleet_role = "${aws_iam_role.test-role.arn}"
spot_price = "0.005"
target_capacity = 4
valid_until = "2019-11-04T20:44:20Z"
terminate_instances_with_expiration = true
launch_specification {
instance_type = "m3.large"
ami = "ami-d0f506b0"
key_name = "${aws_key_pair.debugging.key_name}"
subnet_id = "${aws_subnet.foo.id}"
}
launch_specification {
instance_type = "r3.large"
ami = "ami-d0f506b0"
key_name = "${aws_key_pair.debugging.key_name}"
subnet_id = "${aws_subnet.foo.id}"
}
depends_on = ["aws_iam_policy_attachment.test-attach"]
}
`
const testAccAWSSpotFleetRequestConfigOverridingSpotPrice = `
resource "aws_key_pair" "debugging" {
key_name = "tmp-key"
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 phodgson@thoughtworks.com"
}
resource "aws_iam_policy_attachment" "test-attach" {
name = "test-attachment"
roles = ["${aws_iam_role.test-role.name}"]
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetRole"
}
resource "aws_iam_role" "test-role" {
name = "test-role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "spotfleet.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_spot_fleet_request" "foo" {
iam_fleet_role = "${aws_iam_role.test-role.arn}"
spot_price = "0.005"
target_capacity = 2
valid_until = "2019-11-04T20:44:20Z"
terminate_instances_with_expiration = true
launch_specification {
instance_type = "m1.small"
ami = "ami-d06a90b0"
key_name = "${aws_key_pair.debugging.key_name}"
availability_zone = "us-west-2a"
}
launch_specification {
instance_type = "m3.large"
ami = "ami-d06a90b0"
key_name = "${aws_key_pair.debugging.key_name}"
availability_zone = "us-west-2a"
spot_price = "0.01"
}
depends_on = ["aws_iam_policy_attachment.test-attach"]
}
`
const testAccAWSSpotFleetRequestConfigDiversifiedAllocation = `
resource "aws_key_pair" "debugging" {
key_name = "tmp-key"
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 phodgson@thoughtworks.com"
}
resource "aws_iam_policy_attachment" "test-attach" {
name = "test-attachment"
roles = ["${aws_iam_role.test-role.name}"]
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetRole"
}
resource "aws_iam_role" "test-role" {
name = "test-role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "spotfleet.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_spot_fleet_request" "foo" {
iam_fleet_role = "${aws_iam_role.test-role.arn}"
spot_price = "0.7"
target_capacity = 30
valid_until = "2019-11-04T20:44:20Z"
allocation_strategy = "diversified"
terminate_instances_with_expiration = true
launch_specification {
instance_type = "m1.small"
ami = "ami-d06a90b0"
key_name = "${aws_key_pair.debugging.key_name}"
availability_zone = "us-west-2a"
}
launch_specification {
instance_type = "m3.large"
ami = "ami-d06a90b0"
key_name = "${aws_key_pair.debugging.key_name}"
availability_zone = "us-west-2a"
}
launch_specification {
instance_type = "r3.large"
ami = "ami-d06a90b0"
key_name = "${aws_key_pair.debugging.key_name}"
availability_zone = "us-west-2a"
}
depends_on = ["aws_iam_policy_attachment.test-attach"]
}
`
const testAccAWSSpotFleetRequestConfigWithWeightedCapacity = `
resource "aws_key_pair" "debugging" {
key_name = "tmp-key"
public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 phodgson@thoughtworks.com"
}
resource "aws_iam_policy_attachment" "test-attach" {
name = "test-attachment"
roles = ["${aws_iam_role.test-role.name}"]
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetRole"
}
resource "aws_iam_role" "test-role" {
name = "test-role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "spotfleet.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_spot_fleet_request" "foo" {
iam_fleet_role = "${aws_iam_role.test-role.arn}"
spot_price = "0.7"
target_capacity = 10
valid_until = "2019-11-04T20:44:20Z"
terminate_instances_with_expiration = true
launch_specification {
instance_type = "m3.large"
ami = "ami-d06a90b0"
key_name = "${aws_key_pair.debugging.key_name}"
availability_zone = "us-west-2a"
weighted_capacity = "6"
}
launch_specification {
instance_type = "r3.large"
ami = "ami-d06a90b0"
key_name = "${aws_key_pair.debugging.key_name}"
availability_zone = "us-west-2a"
weighted_capacity = "3"
}
depends_on = ["aws_iam_policy_attachment.test-attach"]
}

View File

@ -42,6 +42,31 @@ resource "aws_spot_fleet_request" "cheap_compute" {
}
```
~> **NOTE:** Terraform does not support the functionality where multiple `subnet_id` or `availability_zone` parameters can be specified in the same
launch configuration block. If you want to specify multiple values, then separate launch configuration blocks should be used:
```
resource "aws_spot_fleet_request" "foo" {
iam_fleet_role = "arn:aws:iam::12345678:role/spot-fleet"
spot_price = "0.005"
target_capacity = 2
valid_until = "2019-11-04T20:44:20Z"
launch_specification {
instance_type = "m1.small"
ami = "ami-d06a90b0"
key_name = "my-key"
availability_zone = "us-west-2a"
}
launch_specification {
instance_type = "m3.large"
ami = "ami-d06a90b0"
key_name = "my-key"
availability_zone = "us-west-2a"
}
depends_on = ["aws_iam_policy_attachment.test-attach"]
}
```
## Argument Reference
Most of these arguments directly correspond to the