Add an AWS Spot fleet resource

This commit is contained in:
Keshav Varma 2016-06-21 17:16:02 -07:00
parent c5ec77eab5
commit 58bd6dfb02
5 changed files with 1314 additions and 0 deletions

View File

@ -257,6 +257,7 @@ func Provider() terraform.ResourceProvider {
"aws_security_group": resourceAwsSecurityGroup(),
"aws_security_group_rule": resourceAwsSecurityGroupRule(),
"aws_spot_instance_request": resourceAwsSpotInstanceRequest(),
"aws_spot_fleet_request": resourceAwsSpotFleetRequest(),
"aws_sqs_queue": resourceAwsSqsQueue(),
"aws_sns_topic": resourceAwsSnsTopic(),
"aws_sns_topic_subscription": resourceAwsSnsTopicSubscription(),

View File

@ -0,0 +1,960 @@
package aws
import (
"bytes"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"fmt"
"log"
"strconv"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsSpotFleetRequest() *schema.Resource {
return &schema.Resource{
Create: resourceAwsSpotFleetRequestCreate,
Read: resourceAwsSpotFleetRequestRead,
Delete: resourceAwsSpotFleetRequestDelete,
Update: resourceAwsSpotFleetRequestUpdate,
Schema: map[string]*schema.Schema{
"iam_fleet_role": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
// http://docs.aws.amazon.com/sdk-for-go/api/service/ec2.html#type-SpotFleetLaunchSpecification
// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_SpotFleetLaunchSpecification.html
"launch_specification": &schema.Schema{
Type: schema.TypeSet,
Required: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"vpc_security_group_ids": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"associate_public_ip_address": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"ebs_block_device": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"delete_on_termination": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
ForceNew: true,
},
"device_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"encrypted": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Computed: true,
ForceNew: true,
},
"iops": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
"snapshot_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_size": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
},
Set: hashEbsBlockDevice,
},
"ephemeral_block_device": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"device_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"virtual_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},
},
Set: hashEphemeralBlockDevice,
},
"root_block_device": &schema.Schema{
// TODO: This is a set because we don't support singleton
// sub-resources today. We'll enforce that the set only ever has
// length zero or one below. When TF gains support for
// sub-resources this can be converted.
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Resource{
// "You can only modify the volume size, volume type, and Delete on
// Termination flag on the block device mapping entry for the root
// device volume." - bit.ly/ec2bdmap
Schema: map[string]*schema.Schema{
"delete_on_termination": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
ForceNew: true,
},
"iops": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_size": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
},
Set: hashRootBlockDevice,
},
"ebs_optimized": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"iam_instance_profile": &schema.Schema{
Type: schema.TypeString,
ForceNew: true,
Optional: true,
},
"ami": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"instance_type": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"key_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
ValidateFunc: validateSpotFleetRequestKeyName,
},
"monitoring": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
// "network_interface_set"
"placement_group": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"spot_price": &schema.Schema{
Type: schema.TypeString,
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,
ForceNew: true,
StateFunc: func(v interface{}) string {
switch v.(type) {
case string:
hash := sha1.Sum([]byte(v.(string)))
return hex.EncodeToString(hash[:])
default:
return ""
}
},
},
"weighted_capacity": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"availability_zone": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},
},
Set: hashLaunchSpecification,
},
// Everything on a spot fleet is ForceNew except target_capacity
"target_capacity": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: false,
},
"allocation_strategy": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "lowestPrice",
ForceNew: true,
},
"excess_capacity_termination_policy": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "Default",
ForceNew: false,
},
"spot_price": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"terminate_instances_with_expiration": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
},
"valid_from": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"valid_until": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"spot_request_state": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"client_token": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
}
}
func buildSpotFleetLaunchSpecification(d map[string]interface{}, meta interface{}) (*ec2.SpotFleetLaunchSpecification, error) {
conn := meta.(*AWSClient).ec2conn
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["ebs_optimized"]; ok {
opts.EbsOptimized = aws.Bool(v.(bool))
}
if v, ok := d["monitoring"]; ok {
opts.Monitoring = &ec2.SpotFleetMonitoring{
Enabled: aws.Bool(v.(bool)),
}
}
if v, ok := d["iam_instance_profile"]; ok {
opts.IamInstanceProfile = &ec2.IamInstanceProfileSpecification{
Name: aws.String(v.(string)),
}
}
if v, ok := d["user_data"]; ok {
opts.UserData = aws.String(
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))
}
if v, ok := d["weighted_capacity"]; ok && v != "" {
wc, err := strconv.ParseFloat(v.(string), 64)
if err != nil {
return nil, err
}
opts.WeightedCapacity = aws.Float64(wc)
}
blockDevices, err := readSpotFleetBlockDeviceMappingsFromConfig(d, conn)
if err != nil {
return nil, err
}
if len(blockDevices) > 0 {
opts.BlockDeviceMappings = blockDevices
}
return opts, nil
}
func validateSpotFleetRequestKeyName(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if value == "" {
errors = append(errors, fmt.Errorf("Key name cannot be empty."))
}
return
}
func readSpotFleetBlockDeviceMappingsFromConfig(
d map[string]interface{}, conn *ec2.EC2) ([]*ec2.BlockDeviceMapping, error) {
blockDevices := make([]*ec2.BlockDeviceMapping, 0)
if v, ok := d["ebs_block_device"]; ok {
vL := v.(*schema.Set).List()
for _, v := range vL {
bd := v.(map[string]interface{})
ebs := &ec2.EbsBlockDevice{
DeleteOnTermination: aws.Bool(bd["delete_on_termination"].(bool)),
}
if v, ok := bd["snapshot_id"].(string); ok && v != "" {
ebs.SnapshotId = aws.String(v)
}
if v, ok := bd["encrypted"].(bool); ok && v {
ebs.Encrypted = aws.Bool(v)
}
if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Int64(int64(v))
}
if v, ok := bd["volume_type"].(string); ok && v != "" {
ebs.VolumeType = aws.String(v)
}
if v, ok := bd["iops"].(int); ok && v > 0 {
ebs.Iops = aws.Int64(int64(v))
}
blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
Ebs: ebs,
})
}
}
if v, ok := d["ephemeral_block_device"]; ok {
vL := v.(*schema.Set).List()
for _, v := range vL {
bd := v.(map[string]interface{})
blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
VirtualName: aws.String(bd["virtual_name"].(string)),
})
}
}
if v, ok := d["root_block_device"]; ok {
vL := v.(*schema.Set).List()
if len(vL) > 1 {
return nil, fmt.Errorf("Cannot specify more than one root_block_device.")
}
for _, v := range vL {
bd := v.(map[string]interface{})
ebs := &ec2.EbsBlockDevice{
DeleteOnTermination: aws.Bool(bd["delete_on_termination"].(bool)),
}
if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Int64(int64(v))
}
if v, ok := bd["volume_type"].(string); ok && v != "" {
ebs.VolumeType = aws.String(v)
}
if v, ok := bd["iops"].(int); ok && v > 0 {
ebs.Iops = aws.Int64(int64(v))
}
if dn, err := fetchRootDeviceName(d["ami"].(string), conn); err == nil {
if dn == nil {
return nil, fmt.Errorf(
"Expected 1 AMI for ID: %s, got none",
d["ami"].(string))
}
blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{
DeviceName: dn,
Ebs: ebs,
})
} else {
return nil, err
}
}
}
return blockDevices, nil
}
func buildAwsSpotFleetLaunchSpecifications(
d *schema.ResourceData, meta interface{}) ([]*ec2.SpotFleetLaunchSpecification, error) {
user_specs := d.Get("launch_specification").(*schema.Set).List()
specs := make([]*ec2.SpotFleetLaunchSpecification, len(user_specs))
for i, user_spec := range user_specs {
user_spec_map := user_spec.(map[string]interface{})
// panic: interface conversion: interface {} is map[string]interface {}, not *schema.ResourceData
opts, err := buildSpotFleetLaunchSpecification(user_spec_map, meta)
if err != nil {
return nil, err
}
specs[i] = opts
}
return specs, nil
}
func resourceAwsSpotFleetRequestCreate(d *schema.ResourceData, meta interface{}) error {
// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RequestSpotFleet.html
conn := meta.(*AWSClient).ec2conn
launch_specs, err := buildAwsSpotFleetLaunchSpecifications(d, meta)
if err != nil {
return err
}
// http://docs.aws.amazon.com/sdk-for-go/api/service/ec2.html#type-SpotFleetRequestConfigData
spotFleetConfig := &ec2.SpotFleetRequestConfigData{
IamFleetRole: aws.String(d.Get("iam_fleet_role").(string)),
LaunchSpecifications: launch_specs,
SpotPrice: aws.String(d.Get("spot_price").(string)),
TargetCapacity: aws.Int64(int64(d.Get("target_capacity").(int))),
ClientToken: aws.String(resource.UniqueId()),
TerminateInstancesWithExpiration: aws.Bool(d.Get("terminate_instances_with_expiration").(bool)),
}
if v, ok := d.GetOk("excess_capacity_termination_policy"); ok {
spotFleetConfig.ExcessCapacityTerminationPolicy = aws.String(v.(string))
}
if v, ok := d.GetOk("allocation_strategy"); ok {
spotFleetConfig.AllocationStrategy = aws.String(v.(string))
} else {
spotFleetConfig.AllocationStrategy = aws.String("lowestPrice")
}
if v, ok := d.GetOk("valid_from"); ok {
valid_from, err := time.Parse(awsAutoscalingScheduleTimeLayout, v.(string))
if err != nil {
return err
}
spotFleetConfig.ValidFrom = &valid_from
}
if v, ok := d.GetOk("valid_until"); ok {
valid_until, err := time.Parse(awsAutoscalingScheduleTimeLayout, v.(string))
if err != nil {
return err
}
spotFleetConfig.ValidUntil = &valid_until
} else {
valid_until := time.Now().Add(24 * time.Hour)
spotFleetConfig.ValidUntil = &valid_until
}
// http://docs.aws.amazon.com/sdk-for-go/api/service/ec2.html#type-RequestSpotFleetInput
spotFleetOpts := &ec2.RequestSpotFleetInput{
SpotFleetRequestConfig: spotFleetConfig,
DryRun: aws.Bool(false),
}
log.Printf("[DEBUG] Requesting spot fleet with these opts: %+v", spotFleetOpts)
// Since IAM is eventually consistent, we retry creation as a newly created role may not
// take effect immediately, resulting in an InvalidSpotFleetRequestConfig error
var resp *ec2.RequestSpotFleetOutput
err = resource.Retry(1*time.Minute, func() *resource.RetryError {
var err error
resp, err = conn.RequestSpotFleet(spotFleetOpts)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
// IAM is eventually consistent :/
if awsErr.Code() == "InvalidSpotFleetRequestConfig" {
return resource.RetryableError(
fmt.Errorf("[WARN] Error creating Spot fleet request, retrying: %s", err))
}
}
return resource.NonRetryableError(err)
}
return nil
})
if err != nil {
return fmt.Errorf("Error requesting spot fleet: %s", err)
}
d.SetId(*resp.SpotFleetRequestId)
return resourceAwsSpotFleetRequestRead(d, meta)
}
func resourceAwsSpotFleetRequestRead(d *schema.ResourceData, meta interface{}) error {
// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSpotFleetRequests.html
conn := meta.(*AWSClient).ec2conn
req := &ec2.DescribeSpotFleetRequestsInput{
SpotFleetRequestIds: []*string{aws.String(d.Id())},
}
resp, err := conn.DescribeSpotFleetRequests(req)
if err != nil {
// If the spot request was not found, return nil so that we can show
// that it is gone.
ec2err, ok := err.(awserr.Error)
if ok && ec2err.Code() == "InvalidSpotFleetRequestID.NotFound" {
d.SetId("")
return nil
}
// Some other error, report it
return err
}
sfr := resp.SpotFleetRequestConfigs[0]
// if the request is cancelled, then it is gone
cancelledStates := map[string]bool{
"cancelled": true,
"cancelled_running": true,
"cancelled_terminating": true,
}
if _, ok := cancelledStates[*sfr.SpotFleetRequestState]; ok {
d.SetId("")
return nil
}
d.SetId(*sfr.SpotFleetRequestId)
d.Set("spot_request_state", aws.StringValue(sfr.SpotFleetRequestState))
config := sfr.SpotFleetRequestConfig
if config.AllocationStrategy != nil {
d.Set("allocation_strategy", aws.StringValue(config.AllocationStrategy))
}
if config.ClientToken != nil {
d.Set("client_token", aws.StringValue(config.ClientToken))
}
if config.ExcessCapacityTerminationPolicy != nil {
d.Set("excess_capacity_termination_policy",
aws.StringValue(config.ExcessCapacityTerminationPolicy))
}
if config.IamFleetRole != nil {
d.Set("iam_fleet_role", aws.StringValue(config.IamFleetRole))
}
if config.SpotPrice != nil {
d.Set("spot_price", aws.StringValue(config.SpotPrice))
}
if config.TargetCapacity != nil {
d.Set("target_capacity", aws.Int64Value(config.TargetCapacity))
}
if config.TerminateInstancesWithExpiration != nil {
d.Set("terminate_instances_with_expiration",
aws.BoolValue(config.TerminateInstancesWithExpiration))
}
if config.ValidFrom != nil {
d.Set("valid_from",
aws.TimeValue(config.ValidFrom).Format(awsAutoscalingScheduleTimeLayout))
}
if config.ValidUntil != nil {
d.Set("valid_until",
aws.TimeValue(config.ValidUntil).Format(awsAutoscalingScheduleTimeLayout))
}
d.Set("launch_specification", launchSpecsToSet(config.LaunchSpecifications, conn))
return nil
}
func launchSpecsToSet(ls []*ec2.SpotFleetLaunchSpecification, conn *ec2.EC2) *schema.Set {
specs := &schema.Set{F: hashLaunchSpecification}
for _, val := range ls {
dn, err := fetchRootDeviceName(aws.StringValue(val.ImageId), conn)
if err != nil {
log.Panic(err)
} else {
ls := launchSpecToMap(val, dn)
specs.Add(ls)
}
}
return specs
}
func launchSpecToMap(
l *ec2.SpotFleetLaunchSpecification,
rootDevName *string,
) map[string]interface{} {
m := make(map[string]interface{})
m["root_block_device"] = rootBlockDeviceToSet(l.BlockDeviceMappings, rootDevName)
m["ebs_block_device"] = ebsBlockDevicesToSet(l.BlockDeviceMappings, rootDevName)
m["ephemeral_block_device"] = ephemeralBlockDevicesToSet(l.BlockDeviceMappings)
if l.ImageId != nil {
m["ami"] = aws.StringValue(l.ImageId)
}
if l.InstanceType != nil {
m["instance_type"] = aws.StringValue(l.InstanceType)
}
if l.SpotPrice != nil {
m["spot_price"] = aws.StringValue(l.SpotPrice)
}
if l.EbsOptimized != nil {
m["ebs_optimized"] = aws.BoolValue(l.EbsOptimized)
}
if l.Monitoring != nil && l.Monitoring.Enabled != nil {
m["monitoring"] = aws.BoolValue(l.Monitoring.Enabled)
}
if l.IamInstanceProfile != nil && l.IamInstanceProfile.Name != nil {
m["iam_instance_profile"] = aws.StringValue(l.IamInstanceProfile.Name)
}
if l.UserData != nil {
ud_dec, err := base64.StdEncoding.DecodeString(aws.StringValue(l.UserData))
if err == nil {
m["user_data"] = string(ud_dec)
}
}
if l.KeyName != nil {
m["key_name"] = aws.StringValue(l.KeyName)
}
if l.Placement != nil {
m["availability_zone"] = aws.StringValue(l.Placement.AvailabilityZone)
}
if l.SubnetId != nil {
m["subnet_id"] = aws.StringValue(l.SubnetId)
}
if l.WeightedCapacity != nil {
m["weighted_capacity"] = fmt.Sprintf("%.3f", aws.Float64Value(l.WeightedCapacity))
}
// m["security_groups"] = securityGroupsToSet(l.SecutiryGroups)
return m
}
func ebsBlockDevicesToSet(bdm []*ec2.BlockDeviceMapping, rootDevName *string) *schema.Set {
set := &schema.Set{F: hashEphemeralBlockDevice}
for _, val := range bdm {
if val.Ebs != nil {
m := make(map[string]interface{})
ebs := val.Ebs
if val.DeviceName != nil {
if aws.StringValue(rootDevName) == aws.StringValue(val.DeviceName) {
continue
}
m["device_name"] = aws.StringValue(val.DeviceName)
}
if ebs.DeleteOnTermination != nil {
m["delete_on_termination"] = aws.BoolValue(ebs.DeleteOnTermination)
}
if ebs.SnapshotId != nil {
m["snapshot_id"] = aws.StringValue(ebs.SnapshotId)
}
if ebs.Encrypted != nil {
m["encrypted"] = aws.BoolValue(ebs.Encrypted)
}
if ebs.VolumeSize != nil {
m["volume_size"] = aws.Int64Value(ebs.VolumeSize)
}
if ebs.VolumeType != nil {
m["volume_type"] = aws.StringValue(ebs.VolumeType)
}
if ebs.Iops != nil {
m["iops"] = aws.Int64Value(ebs.Iops)
}
set.Add(m)
}
}
return set
}
func ephemeralBlockDevicesToSet(bdm []*ec2.BlockDeviceMapping) *schema.Set {
set := &schema.Set{F: hashEphemeralBlockDevice}
for _, val := range bdm {
if val.VirtualName != nil {
m := make(map[string]interface{})
m["virtual_name"] = aws.StringValue(val.VirtualName)
if val.DeviceName != nil {
m["device_name"] = aws.StringValue(val.DeviceName)
}
set.Add(m)
}
}
return set
}
func rootBlockDeviceToSet(
bdm []*ec2.BlockDeviceMapping,
rootDevName *string,
) *schema.Set {
set := &schema.Set{F: hashRootBlockDevice}
if rootDevName != nil {
for _, val := range bdm {
if aws.StringValue(val.DeviceName) == aws.StringValue(rootDevName) {
m := make(map[string]interface{})
if val.Ebs.DeleteOnTermination != nil {
m["delete_on_termination"] = aws.BoolValue(val.Ebs.DeleteOnTermination)
}
if val.Ebs.VolumeSize != nil {
m["volume_size"] = aws.Int64Value(val.Ebs.VolumeSize)
}
if val.Ebs.VolumeType != nil {
m["volume_type"] = aws.StringValue(val.Ebs.VolumeType)
}
if val.Ebs.Iops != nil {
m["iops"] = aws.Int64Value(val.Ebs.Iops)
}
set.Add(m)
}
}
}
return set
}
func resourceAwsSpotFleetRequestUpdate(d *schema.ResourceData, meta interface{}) error {
// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ModifySpotFleetRequest.html
conn := meta.(*AWSClient).ec2conn
d.Partial(true)
req := &ec2.ModifySpotFleetRequestInput{
SpotFleetRequestId: aws.String(d.Id()),
}
if val, ok := d.GetOk("target_capacity"); ok {
req.TargetCapacity = aws.Int64(int64(val.(int)))
}
if val, ok := d.GetOk("excess_capacity_termination_policy"); ok {
req.ExcessCapacityTerminationPolicy = aws.String(val.(string))
}
resp, err := conn.ModifySpotFleetRequest(req)
if err == nil && aws.BoolValue(resp.Return) {
// TODO: rollback to old values?
}
return nil
}
func resourceAwsSpotFleetRequestDelete(d *schema.ResourceData, meta interface{}) error {
// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CancelSpotFleetRequests.html
conn := meta.(*AWSClient).ec2conn
log.Printf("[INFO] Cancelling spot fleet request: %s", d.Id())
_, err := conn.CancelSpotFleetRequests(&ec2.CancelSpotFleetRequestsInput{
SpotFleetRequestIds: []*string{aws.String(d.Id())},
TerminateInstances: aws.Bool(d.Get("terminate_instances_with_expiration").(bool)),
})
if err != nil {
return fmt.Errorf("Error cancelling spot request (%s): %s", d.Id(), err)
}
return nil
}
func hashEphemeralBlockDevice(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string)))
return hashcode.String(buf.String())
}
func hashRootBlockDevice(v interface{}) int {
// there can be only one root device; no need to hash anything
return 0
}
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"] != "" {
buf.WriteString(fmt.Sprintf("%s-", m["availability_zone"].(string)))
} else if m["subnet_id"] != nil && m["subnet_id"] != "" {
buf.WriteString(fmt.Sprintf("%s-", m["subnet_id"].(string)))
} else {
panic(
fmt.Sprintf(
"Must set one of:\navailability_zone %#v\nsubnet_id: %#v",
m["availability_zone"],
m["subnet_id"]))
}
buf.WriteString(fmt.Sprintf("%s-", m["instance_type"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["spot_price"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["user_data"].(string)))
return hashcode.String(buf.String())
}
func hashEbsBlockDevice(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string)))
return hashcode.String(buf.String())
}

View File

@ -0,0 +1,263 @@
package aws
import (
"encoding/base64"
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSSpotFleetRequest_basic(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: testAccAWSSpotFleetRequestConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSpotFleetRequestExists(
"aws_spot_fleet_request.foo", &sfr),
testAccCheckAWSSpotFleetRequestAttributes(&sfr),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_request_state", "active"),
),
},
},
})
}
func TestAccAWSSpotFleetRequest_launchConfiguration(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: testAccAWSSpotFleetRequestWithAdvancedLaunchSpecConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSpotFleetRequestExists(
"aws_spot_fleet_request.foo", &sfr),
testAccCheckAWSSpotFleetRequest_LaunchSpecAttributes(&sfr),
resource.TestCheckResourceAttr(
"aws_spot_fleet_request.foo", "spot_request_state", "active"),
),
},
},
})
}
func TestAccAWSSpotFleetRequest_CannotUseEmptyKeyName(t *testing.T) {
_, errors := validateSpotFleetRequestKeyName("", "key_name")
if len(errors) == 0 {
t.Fatalf("Expected the key name to trigger a validation error")
}
}
func testAccCheckAWSSpotFleetRequestExists(
n string, sfr *ec2.SpotFleetRequestConfig) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}
if rs.Primary.ID == "" {
return fmt.Errorf("No Spot fleet request with that id exists")
}
conn := testAccProvider.Meta().(*AWSClient).ec2conn
params := &ec2.DescribeSpotFleetRequestsInput{
SpotFleetRequestIds: []*string{&rs.Primary.ID},
}
resp, err := conn.DescribeSpotFleetRequests(params)
if err != nil {
return err
}
if v := len(resp.SpotFleetRequestConfigs); v != 1 {
return fmt.Errorf("Expected 1 request returned, got %d", v)
}
*sfr = *resp.SpotFleetRequestConfigs[0]
return nil
}
}
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 {
if len(sfr.SpotFleetRequestConfig.LaunchSpecifications) == 0 {
return fmt.Errorf("Missing launch specification")
}
spec := *sfr.SpotFleetRequestConfig.LaunchSpecifications[0]
if *spec.InstanceType != "c3.large" {
return fmt.Errorf("Unexpected launch specification instance type: %s", *spec.InstanceType)
}
if *spec.ImageId != "ami-f652979b" {
return fmt.Errorf("Unexpected launch specification image id: %s", *spec.ImageId)
}
if *spec.SpotPrice != "0.01" {
return fmt.Errorf("Unexpected launch specification spot price: %s", *spec.SpotPrice)
}
if *spec.WeightedCapacity != 2 {
return fmt.Errorf("Unexpected launch specification weighted capacity: %f", *spec.WeightedCapacity)
}
if *spec.UserData != base64.StdEncoding.EncodeToString([]byte("hello-world")) {
return fmt.Errorf("Unexpected launch specification user data: %s", *spec.UserData)
}
return nil
}
}
func testAccCheckAWSSpotFleetRequestDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_spot_fleet_request" {
continue
}
_, err := conn.CancelSpotFleetRequests(&ec2.CancelSpotFleetRequestsInput{
SpotFleetRequestIds: []*string{aws.String(rs.Primary.ID)},
TerminateInstances: aws.Bool(true),
})
if err != nil {
return fmt.Errorf("Error cancelling spot request (%s): %s", rs.Primary.ID, err)
}
}
return nil
}
const testAccAWSSpotFleetRequestConfig = `
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"
launch_specification {
instance_type = "c3.large"
ami = "ami-f652979b"
key_name = "${aws_key_pair.debugging.key_name}"
availability_zone = "us-east-1a"
}
depends_on = ["aws_iam_policy_attachment.test-attach"]
}
`
const testAccAWSSpotFleetRequestWithAdvancedLaunchSpecConfig = `
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 = 4
valid_until = "2019-11-04T20:44:20Z"
allocation_strategy = "diversified"
launch_specification {
instance_type = "c3.large"
ami = "ami-f652979b"
key_name = "${aws_key_pair.debugging.key_name}"
availability_zone = "us-east-1a"
spot_price = "0.01"
weighted_capacity = 2
user_data = "hello-world"
root_block_device {
volume_size = "300"
volume_type = "gp2"
}
}
depends_on = ["aws_iam_policy_attachment.test-attach"]
}
`

View File

@ -0,0 +1,86 @@
---
layout: "aws"
page_title: "AWS: aws_spot_fleet_request"
sidebar_current: "docs-aws-resource-spot-fleet-request"
description: |-
Provides a Spot Fleet Request resource.
---
# aws\_spot\_fleet\_request
Provides an EC2 Spot Fleet Request resource. This allows a fleet of Spot
instances to be requested on the Spot market.
## Example Usage
```
# Request a Spot fleet
resource "aws_spot_fleet_request" "cheap_compute" {
iam_fleet_role = "arn:aws:iam::12345678:role/spot-fleet"
spot_price = "0.03"
allocation_strategy = "diversified"
target_capacity = 6
valid_until = "2019-11-04T20:44:20Z"
launch_specification {
instance_type = "m4.10xlarge"
ami = "ami-1234"
spot_price = "2.793"
}
launch_specification {
instance_type = "m4.4xlarge"
ami = "ami-5678"
key_name = "my-key"
spot_price = "1.117"
availability_zone = "us-west-1a"
subnet_id = "subnet-1234"
weighted_capacity = 35
root_block_device {
volume_size = "300"
volume_type = "gp2"
}
}
}
```
## Argument Reference
Most of these arguments directly correspond to the
[offical API](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_SpotFleetRequestConfigData.html).
* `iam_fleet_role` - (Required) Grants the Spot fleet permission to terminate
Spot instances on your behalf when you cancel its Spot fleet request using
CancelSpotFleetRequests or when the Spot fleet request expires, if you set
terminateInstancesWithExpiration.
* `launch_specification` - Used to define the launch configuration of the
spot-fleet request. Can be specified multiple times to define different bids
across different markets and instance types.
**Note:** This takes in similar but not
identical inputs as [`aws_instance`](instance.html). There are limitations on
what you can specify (tags, for example, are not supported). See the
list of officially supported inputs in the
[reference documentation](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_SpotFleetLaunchSpecification.html). Any normal [`aws_instance`](instance.html) parameter that corresponds to those inputs may be used.
* `spot_price` - (Required) The bid price per unit hour.
* `target_capacity` - The number of units to request. You can choose to set the
target capacity in terms of instances or a performance characteristic that is
important to your application workload, such as vCPUs, memory, or I/O.
* `allocation_strategy` - Indicates how to allocate the target capacity across
the Spot pools specified by the Spot fleet request. The default is
lowestPrice.
* `excess_capacity_termination_policy` - Indicates whether running Spot
instances should be terminated if the target capacity of the Spot fleet
request is decreased below the current size of the Spot fleet.
* `terminate_instances_with_expiration` - Indicates whether running Spot
instances should be terminated when the Spot fleet request expires.
* `valid_until` - The end date and time of the request, in UTC ISO8601 format
(for example, YYYY-MM-DDTHH:MM:SSZ). At this point, no new Spot instance
requests are placed or enabled to fulfill the request. Defaults to 24 hours.
## Attributes Reference
The following attributes are exported:
* `id` - The Spot fleet request ID
* `spot_request_state` - The state of the Spot fleet request.

View File

@ -264,6 +264,10 @@
<a href="/docs/providers/aws/r/spot_instance_request.html">aws_spot_instance_request</a>
</li>
<li<%= sidebar_current("docs-aws-resource-spot-fleet-request") %>>
<a href="/docs/providers/aws/r/spot_fleet_request.html">aws_spot_fleet_request</a>
</li>
<li<%= sidebar_current("docs-aws-resource-volume-attachment") %>>
<a href="/docs/providers/aws/r/volume_attachment.html">aws_volume_attachment</a>
</li>