Merge pull request #2263 from hashicorp/f-aws-spot-instance-request

provider/aws: spot_instance_request
This commit is contained in:
Paul Hinze 2015-06-08 10:29:58 -05:00
commit e305d7c5df
6 changed files with 754 additions and 226 deletions

View File

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

View File

@ -12,6 +12,7 @@ import (
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
@ -37,8 +38,9 @@ func resourceAwsInstance() *schema.Resource {
"associate_public_ip_address": &schema.Schema{ "associate_public_ip_address": &schema.Schema{
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Default: false,
ForceNew: true, ForceNew: true,
Optional: true,
}, },
"availability_zone": &schema.Schema{ "availability_zone": &schema.Schema{
@ -313,214 +315,32 @@ func resourceAwsInstance() *schema.Resource {
func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn conn := meta.(*AWSClient).ec2conn
// Figure out user data instanceOpts, err := buildAwsInstanceOpts(d, meta)
userData := "" if err != nil {
if v := d.Get("user_data"); v != nil { return err
userData = base64.StdEncoding.EncodeToString([]byte(v.(string)))
}
// check for non-default Subnet, and cast it to a String
var hasSubnet bool
subnet, hasSubnet := d.GetOk("subnet_id")
subnetID := subnet.(string)
placement := &ec2.Placement{
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
GroupName: aws.String(d.Get("placement_group").(string)),
}
if hasSubnet {
// Tenancy is only valid inside a VPC
// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_Placement.html
if v := d.Get("tenancy").(string); v != "" {
placement.Tenancy = aws.String(v)
}
}
iam := &ec2.IAMInstanceProfileSpecification{
Name: aws.String(d.Get("iam_instance_profile").(string)),
} }
// Build the creation struct // Build the creation struct
runOpts := &ec2.RunInstancesInput{ runOpts := &ec2.RunInstancesInput{
ImageID: aws.String(d.Get("ami").(string)), BlockDeviceMappings: instanceOpts.BlockDeviceMappings,
Placement: placement, DisableAPITermination: instanceOpts.DisableAPITermination,
InstanceType: aws.String(d.Get("instance_type").(string)), EBSOptimized: instanceOpts.EBSOptimized,
IAMInstanceProfile: instanceOpts.IAMInstanceProfile,
ImageID: instanceOpts.ImageID,
InstanceType: instanceOpts.InstanceType,
MaxCount: aws.Long(int64(1)), MaxCount: aws.Long(int64(1)),
MinCount: aws.Long(int64(1)), MinCount: aws.Long(int64(1)),
UserData: aws.String(userData), NetworkInterfaces: instanceOpts.NetworkInterfaces,
EBSOptimized: aws.Boolean(d.Get("ebs_optimized").(bool)), Placement: instanceOpts.Placement,
DisableAPITermination: aws.Boolean(d.Get("disable_api_termination").(bool)), PrivateIPAddress: instanceOpts.PrivateIPAddress,
IAMInstanceProfile: iam, SecurityGroupIDs: instanceOpts.SecurityGroupIDs,
} SecurityGroups: instanceOpts.SecurityGroups,
SubnetID: instanceOpts.SubnetID,
associatePublicIPAddress := false UserData: instanceOpts.UserData64,
if v := d.Get("associate_public_ip_address"); v != nil {
associatePublicIPAddress = v.(bool)
}
var groups []*string
if v := d.Get("security_groups"); v != nil {
// 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.Boolean(associatePublicIPAddress),
DeviceIndex: aws.Long(int64(0)),
SubnetID: aws.String(subnetID),
Groups: groups,
}
if v, ok := d.GetOk("private_ip"); ok {
ni.PrivateIPAddress = aws.String(v.(string))
}
if v := d.Get("vpc_security_group_ids"); v != nil {
for _, v := range v.(*schema.Set).List() {
ni.Groups = append(ni.Groups, aws.String(v.(string)))
}
}
runOpts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni}
} else {
if subnetID != "" {
runOpts.SubnetID = aws.String(subnetID)
}
if v, ok := d.GetOk("private_ip"); ok {
runOpts.PrivateIPAddress = aws.String(v.(string))
}
if runOpts.SubnetID != nil &&
*runOpts.SubnetID != "" {
runOpts.SecurityGroupIDs = groups
} else {
runOpts.SecurityGroups = groups
}
if v := d.Get("vpc_security_group_ids"); v != nil {
for _, v := range v.(*schema.Set).List() {
runOpts.SecurityGroupIDs = append(runOpts.SecurityGroupIDs, aws.String(v.(string)))
}
}
}
if v, ok := d.GetOk("key_name"); ok {
runOpts.KeyName = aws.String(v.(string))
}
blockDevices := make([]*ec2.BlockDeviceMapping, 0)
if v, ok := d.GetOk("ebs_block_device"); ok {
vL := v.(*schema.Set).List()
for _, v := range vL {
bd := v.(map[string]interface{})
ebs := &ec2.EBSBlockDevice{
DeleteOnTermination: aws.Boolean(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.Boolean(v)
}
if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Long(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.Long(int64(v))
}
blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
EBS: ebs,
})
}
}
if v, ok := d.GetOk("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.GetOk("root_block_device"); ok {
vL := v.(*schema.Set).List()
if len(vL) > 1 {
return fmt.Errorf("Cannot specify more than one root_block_device.")
}
for _, v := range vL {
bd := v.(map[string]interface{})
ebs := &ec2.EBSBlockDevice{
DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)),
}
if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Long(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.Long(int64(v))
}
if dn, err := fetchRootDeviceName(d.Get("ami").(string), conn); err == nil {
if dn == nil {
return fmt.Errorf(
"Expected 1 AMI for ID: %s, got none",
d.Get("ami").(string))
}
blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{
DeviceName: dn,
EBS: ebs,
})
} else {
return err
}
}
}
if len(blockDevices) > 0 {
runOpts.BlockDeviceMappings = blockDevices
} }
// Create the instance // Create the instance
log.Printf("[DEBUG] Run configuration: %#v", runOpts) log.Printf("[DEBUG] Run configuration: %s", awsutil.StringValue(runOpts))
var err error
var runResp *ec2.Reservation var runResp *ec2.Reservation
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
@ -756,32 +576,8 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
func resourceAwsInstanceDelete(d *schema.ResourceData, meta interface{}) error { func resourceAwsInstanceDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn conn := meta.(*AWSClient).ec2conn
log.Printf("[INFO] Terminating instance: %s", d.Id()) if err := awsTerminateInstance(conn, d.Id()); err != nil {
req := &ec2.TerminateInstancesInput{ return err
InstanceIDs: []*string{aws.String(d.Id())},
}
if _, err := conn.TerminateInstances(req); err != nil {
return fmt.Errorf("Error terminating instance: %s", err)
}
log.Printf(
"[DEBUG] Waiting for instance (%s) to become terminated",
d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Target: "terminated",
Refresh: InstanceStateRefreshFunc(conn, d.Id()),
Timeout: 10 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}
_, err := stateConf.WaitForState()
if err != nil {
return fmt.Errorf(
"Error waiting for instance (%s) to terminate: %s",
d.Id(), err)
} }
d.SetId("") d.SetId("")
@ -926,3 +722,261 @@ func fetchRootDeviceName(ami string, conn *ec2.EC2) (*string, error) {
return nil, err return nil, err
} }
} }
func readBlockDeviceMappingsFromConfig(
d *schema.ResourceData, conn *ec2.EC2) ([]*ec2.BlockDeviceMapping, error) {
blockDevices := make([]*ec2.BlockDeviceMapping, 0)
if v, ok := d.GetOk("ebs_block_device"); ok {
vL := v.(*schema.Set).List()
for _, v := range vL {
bd := v.(map[string]interface{})
ebs := &ec2.EBSBlockDevice{
DeleteOnTermination: aws.Boolean(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.Boolean(v)
}
if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Long(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.Long(int64(v))
}
blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
EBS: ebs,
})
}
}
if v, ok := d.GetOk("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.GetOk("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.Boolean(bd["delete_on_termination"].(bool)),
}
if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Long(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.Long(int64(v))
}
if dn, err := fetchRootDeviceName(d.Get("ami").(string), conn); err == nil {
if dn == nil {
return nil, fmt.Errorf(
"Expected 1 AMI for ID: %s, got none",
d.Get("ami").(string))
}
blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{
DeviceName: dn,
EBS: ebs,
})
} else {
return nil, err
}
}
}
return blockDevices, nil
}
type awsInstanceOpts struct {
BlockDeviceMappings []*ec2.BlockDeviceMapping
DisableAPITermination *bool
EBSOptimized *bool
IAMInstanceProfile *ec2.IAMInstanceProfileSpecification
ImageID *string
InstanceType *string
KeyName *string
NetworkInterfaces []*ec2.InstanceNetworkInterfaceSpecification
Placement *ec2.Placement
PrivateIPAddress *string
SecurityGroupIDs []*string
SecurityGroups []*string
SpotPlacement *ec2.SpotPlacement
SubnetID *string
UserData64 *string
}
func buildAwsInstanceOpts(
d *schema.ResourceData, meta interface{}) (*awsInstanceOpts, error) {
conn := meta.(*AWSClient).ec2conn
opts := &awsInstanceOpts{
DisableAPITermination: aws.Boolean(d.Get("disable_api_termination").(bool)),
EBSOptimized: aws.Boolean(d.Get("ebs_optimized").(bool)),
ImageID: aws.String(d.Get("ami").(string)),
InstanceType: aws.String(d.Get("instance_type").(string)),
}
opts.IAMInstanceProfile = &ec2.IAMInstanceProfileSpecification{
Name: aws.String(d.Get("iam_instance_profile").(string)),
}
opts.UserData64 = aws.String(
base64.StdEncoding.EncodeToString([]byte(d.Get("user_data").(string))))
// check for non-default Subnet, and cast it to a String
subnet, hasSubnet := d.GetOk("subnet_id")
subnetID := subnet.(string)
// Placement is used for aws_instance; SpotPlacement is used for
// aws_spot_instance_request. They represent the same data. :-|
opts.Placement = &ec2.Placement{
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
GroupName: aws.String(d.Get("placement_group").(string)),
}
opts.SpotPlacement = &ec2.SpotPlacement{
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
GroupName: aws.String(d.Get("placement_group").(string)),
}
if v := d.Get("tenancy").(string); v != "" {
opts.Placement.Tenancy = aws.String(v)
}
associatePublicIPAddress := d.Get("associate_public_ip_address").(bool)
var groups []*string
if v := d.Get("security_groups"); v != nil {
// 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.Boolean(associatePublicIPAddress),
DeviceIndex: aws.Long(int64(0)),
SubnetID: aws.String(subnetID),
Groups: groups,
}
if v, ok := d.GetOk("private_ip"); ok {
ni.PrivateIPAddress = aws.String(v.(string))
}
if v := d.Get("vpc_security_group_ids"); v != nil {
for _, v := range v.(*schema.Set).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.GetOk("private_ip"); ok {
opts.PrivateIPAddress = aws.String(v.(string))
}
if opts.SubnetID != nil &&
*opts.SubnetID != "" {
opts.SecurityGroupIDs = groups
} else {
opts.SecurityGroups = groups
}
if v := d.Get("vpc_security_group_ids"); v != nil {
for _, v := range v.(*schema.Set).List() {
opts.SecurityGroupIDs = append(opts.SecurityGroupIDs, aws.String(v.(string)))
}
}
}
if v, ok := d.GetOk("key_name"); ok {
opts.KeyName = aws.String(v.(string))
}
blockDevices, err := readBlockDeviceMappingsFromConfig(d, conn)
if err != nil {
return nil, err
}
if len(blockDevices) > 0 {
opts.BlockDeviceMappings = blockDevices
}
return opts, nil
}
func awsTerminateInstance(conn *ec2.EC2, id string) error {
log.Printf("[INFO] Terminating instance: %s", id)
req := &ec2.TerminateInstancesInput{
InstanceIDs: []*string{aws.String(id)},
}
if _, err := conn.TerminateInstances(req); err != nil {
return fmt.Errorf("Error terminating instance: %s", err)
}
log.Printf("[DEBUG] Waiting for instance (%s) to become terminated", id)
stateConf := &resource.StateChangeConf{
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Target: "terminated",
Refresh: InstanceStateRefreshFunc(conn, id),
Timeout: 10 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}
_, err := stateConf.WaitForState()
if err != nil {
return fmt.Errorf(
"Error waiting for instance (%s) to terminate: %s", id, err)
}
return nil
}

View File

@ -0,0 +1,240 @@
package aws
import (
"fmt"
"log"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsSpotInstanceRequest() *schema.Resource {
return &schema.Resource{
Create: resourceAwsSpotInstanceRequestCreate,
Read: resourceAwsSpotInstanceRequestRead,
Delete: resourceAwsSpotInstanceRequestDelete,
Update: resourceAwsSpotInstanceRequestUpdate,
Schema: func() map[string]*schema.Schema {
// The Spot Instance Request Schema is based on the AWS Instance schema.
s := resourceAwsInstance().Schema
// Everything on a spot instance is ForceNew except tags
for k, v := range s {
if k == "tags" {
continue
}
v.ForceNew = true
}
s["spot_price"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
}
s["wait_for_fulfillment"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
}
s["spot_bid_status"] = &schema.Schema{
Type: schema.TypeString,
Computed: true,
}
s["spot_request_state"] = &schema.Schema{
Type: schema.TypeString,
Computed: true,
}
s["spot_instance_id"] = &schema.Schema{
Type: schema.TypeString,
Computed: true,
}
return s
}(),
}
}
func resourceAwsSpotInstanceRequestCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
instanceOpts, err := buildAwsInstanceOpts(d, meta)
if err != nil {
return err
}
spotOpts := &ec2.RequestSpotInstancesInput{
SpotPrice: aws.String(d.Get("spot_price").(string)),
// We always set the type to "persistent", since the imperative-like
// behavior of "one-time" does not map well to TF's declarative domain.
Type: aws.String("persistent"),
// Though the AWS API supports creating spot instance requests for multiple
// instances, for TF purposes we fix this to one instance per request.
// Users can get equivalent behavior out of TF's "count" meta-parameter.
InstanceCount: aws.Long(1),
LaunchSpecification: &ec2.RequestSpotLaunchSpecification{
BlockDeviceMappings: instanceOpts.BlockDeviceMappings,
EBSOptimized: instanceOpts.EBSOptimized,
IAMInstanceProfile: instanceOpts.IAMInstanceProfile,
ImageID: instanceOpts.ImageID,
InstanceType: instanceOpts.InstanceType,
Placement: instanceOpts.SpotPlacement,
SecurityGroupIDs: instanceOpts.SecurityGroupIDs,
SecurityGroups: instanceOpts.SecurityGroups,
UserData: instanceOpts.UserData64,
},
}
// Make the spot instance request
resp, err := conn.RequestSpotInstances(spotOpts)
if err != nil {
return fmt.Errorf("Error requesting spot instances: %s", err)
}
if len(resp.SpotInstanceRequests) != 1 {
return fmt.Errorf(
"Expected response with length 1, got: %s", awsutil.StringValue(resp))
}
sir := *resp.SpotInstanceRequests[0]
d.SetId(*sir.SpotInstanceRequestID)
if d.Get("wait_for_fulfillment").(bool) {
spotStateConf := &resource.StateChangeConf{
// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-bid-status.html
Pending: []string{"start", "pending-evaluation", "pending-fulfillment"},
Target: "fulfilled",
Refresh: SpotInstanceStateRefreshFunc(conn, sir),
Timeout: 10 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}
log.Printf("[DEBUG] waiting for spot bid to resolve... this may take several minutes.")
_, err = spotStateConf.WaitForState()
if err != nil {
return fmt.Errorf("Error while waiting for spot request (%s) to resolve: %s", awsutil.StringValue(sir), err)
}
}
return resourceAwsSpotInstanceRequestUpdate(d, meta)
}
// Update spot state, etc
func resourceAwsSpotInstanceRequestRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
req := &ec2.DescribeSpotInstanceRequestsInput{
SpotInstanceRequestIDs: []*string{aws.String(d.Id())},
}
resp, err := conn.DescribeSpotInstanceRequests(req)
if err != nil {
// If the spot request was not found, return nil so that we can show
// that it is gone.
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" {
d.SetId("")
return nil
}
// Some other error, report it
return err
}
// If nothing was found, then return no state
if len(resp.SpotInstanceRequests) == 0 {
d.SetId("")
return nil
}
request := resp.SpotInstanceRequests[0]
// if the request is cancelled, then it is gone
if *request.State == "canceled" {
d.SetId("")
return nil
}
d.Set("spot_bid_status", *request.Status.Code)
d.Set("spot_instance_id", *request.InstanceID)
d.Set("spot_request_state", *request.State)
d.Set("tags", tagsToMap(request.Tags))
return nil
}
func resourceAwsSpotInstanceRequestUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
d.Partial(true)
if err := setTags(conn, d); err != nil {
return err
} else {
d.SetPartial("tags")
}
d.Partial(false)
return resourceAwsInstanceRead(d, meta)
}
func resourceAwsSpotInstanceRequestDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
log.Printf("[INFO] Cancelling spot request: %s", d.Id())
_, err := conn.CancelSpotInstanceRequests(&ec2.CancelSpotInstanceRequestsInput{
SpotInstanceRequestIDs: []*string{aws.String(d.Id())},
})
if err != nil {
return fmt.Errorf("Error cancelling spot request (%s): %s", d.Id(), err)
}
if instanceId := d.Get("spot_instance_id").(string); instanceId != "" {
log.Printf("[INFO] Terminating instance: %s", instanceId)
if err := awsTerminateInstance(conn, instanceId); err != nil {
return fmt.Errorf("Error terminating spot instance: %s", err)
}
}
return nil
}
// SpotInstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
// an EC2 spot instance request
func SpotInstanceStateRefreshFunc(
conn *ec2.EC2, sir ec2.SpotInstanceRequest) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{
SpotInstanceRequestIDs: []*string{sir.SpotInstanceRequestID},
})
if err != nil {
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" {
// Set this to nil as if we didn't find anything.
resp = nil
} else {
log.Printf("Error on StateRefresh: %s", err)
return nil, "", err
}
}
if resp == nil || len(resp.SpotInstanceRequests) == 0 {
// Sometimes AWS just has consistency issues and doesn't see
// our request yet. Return an empty state.
return nil, "", nil
}
req := resp.SpotInstanceRequests[0]
return req, *req.Status.Code, nil
}
}

View File

@ -0,0 +1,158 @@
package aws
import (
"fmt"
"testing"
"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/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSSpotInstanceRequest_basic(t *testing.T) {
var sir ec2.SpotInstanceRequest
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSpotInstanceRequestDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSSpotInstanceRequestConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSpotInstanceRequestExists(
"aws_spot_instance_request.foo", &sir),
testAccCheckAWSSpotInstanceRequestAttributes(&sir),
resource.TestCheckResourceAttr(
"aws_spot_instance_request.foo", "spot_bid_status", "fulfilled"),
resource.TestCheckResourceAttr(
"aws_spot_instance_request.foo", "spot_request_state", "active"),
),
},
},
})
}
func testAccCheckAWSSpotInstanceRequestDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_spot_instance_request" {
continue
}
req := &ec2.DescribeSpotInstanceRequestsInput{
SpotInstanceRequestIDs: []*string{aws.String(rs.Primary.ID)},
}
resp, err := conn.DescribeSpotInstanceRequests(req)
if err == nil {
if len(resp.SpotInstanceRequests) > 0 {
return fmt.Errorf("Spot instance request is still here.")
}
}
// Verify the error is what we expect
ec2err, ok := err.(awserr.Error)
if !ok {
return err
}
if ec2err.Code() != "InvalidSpotInstanceRequestID.NotFound" {
return err
}
// Now check if the associated Spot Instance was also destroyed
instId := rs.Primary.Attributes["spot_instance_id"]
instResp, instErr := conn.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIDs: []*string{aws.String(instId)},
})
if instErr == nil {
if len(instResp.Reservations) > 0 {
return fmt.Errorf("Instance still exists.")
}
return nil
}
// Verify the error is what we expect
ec2err, ok = err.(awserr.Error)
if !ok {
return err
}
if ec2err.Code() != "InvalidInstanceID.NotFound" {
return err
}
}
return nil
}
func testAccCheckAWSSpotInstanceRequestExists(
n string, sir *ec2.SpotInstanceRequest) 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 SNS subscription with that ARN exists")
}
conn := testAccProvider.Meta().(*AWSClient).ec2conn
params := &ec2.DescribeSpotInstanceRequestsInput{
SpotInstanceRequestIDs: []*string{&rs.Primary.ID},
}
resp, err := conn.DescribeSpotInstanceRequests(params)
if err != nil {
return err
}
if v := len(resp.SpotInstanceRequests); v != 1 {
return fmt.Errorf("Expected 1 request returned, got %d", v)
}
*sir = *resp.SpotInstanceRequests[0]
return nil
}
}
func testAccCheckAWSSpotInstanceRequestAttributes(
sir *ec2.SpotInstanceRequest) resource.TestCheckFunc {
return func(s *terraform.State) error {
if *sir.SpotPrice != "0.050000" {
return fmt.Errorf("Unexpected spot price: %s", *sir.SpotPrice)
}
if *sir.State != "active" {
return fmt.Errorf("Unexpected request state: %s", *sir.State)
}
if *sir.Status.Code != "fulfilled" {
return fmt.Errorf("Unexpected bid status: %s", *sir.State)
}
return nil
}
}
const testAccAWSSpotInstanceRequestConfig = `
resource "aws_spot_instance_request" "foo" {
ami = "ami-4fccb37f"
instance_type = "m1.small"
// base price is $0.044 hourly, so bidding above that should theoretically
// always fulfill
spot_price = "0.05"
// we wait for fulfillment because we want to inspect the launched instance
// and verify termination behavior
wait_for_fulfillment = true
tags {
Name = "terraform-test"
}
}
`

View File

@ -0,0 +1,71 @@
---
layout: "aws"
page_title: "AWS: aws_spot_instance_request"
sidebar_current: "docs-aws-resource-spot-instance-request"
description: |-
Provides a Spot Instance Request resource.
---
# aws\_spot\_instance\_request
Provides an EC2 Spot Instance Request resource. This allows instances to be
requested on the spot market.
Terraform always creates Spot Instance Requests with a `persistent` type, which
means that for the duration of their lifetime, AWS will launch an instance
with the configured details if and when the spot market will accept the
requested price.
On destruction, Terraform will make an attempt to terminate the associated Spot
Instance if there is one present.
~> **NOTE:** Because their behavior depends on the live status of the spot
market, Spot Instance Requests have a unique lifecycle that makes them behave
differently than other Terraform resources. Most importantly: there is __no
guarantee__ that a Spot Instance exists to fulfill the request at any given
point in time. See the [AWS Spot Instance
documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-spot-instances.html)
for more information.
## Example Usage
```
# Request a spot instance at $0.03
resource "aws_spot_instance_request" "cheap_worker" {
ami = "ami-1234"
spot_price = "0.03"
instance_type = "c4.xlarge"
tags {
Name = "CheapWorker"
}
}
```
## Argument Reference
Spot Instance Requests support all the same arguments as
[`aws_instance`](instance.html), with the addition of:
* `spot_price` - (Required) The price to request on the spot market.
* `wait_for_fulfillment` - (Optional; Default: false) If set, Terraform will
wait for the Spot Request to be fulfilled, and will throw an error if the
timeout of 10m is reached.
## Attributes Reference
The following attributes are exported:
* `id` - The Spot Instance Request ID.
These attributes are exported, but they are expected to change over time and so
should only be used for informational purposes, not for resource dependencies:
* `spot_bid_status` - The current [bid
status](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-bid-status.html)
of the Spot Instance Request.
* `spot_request_state` The current [request
state](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-requests.html#creating-spot-request-status)
of the Spot Instance Request.
* `spot_instance_id` - The Instance ID (if any) that is currently fulfilling
the Spot Instance request.

View File

@ -197,6 +197,10 @@
<a href="/docs/providers/aws/r/sns_topic_subscription.html">aws_sns_topic_subscription</a> <a href="/docs/providers/aws/r/sns_topic_subscription.html">aws_sns_topic_subscription</a>
</li> </li>
<li<%= sidebar_current("docs-aws-resource-spot-instance-request") %>>
<a href="/docs/providers/aws/r/spot_instance_request.html">aws_spot_instance_request</a>
</li>
<li<%= sidebar_current("docs-aws-resource-sqs-queue") %>> <li<%= sidebar_current("docs-aws-resource-sqs-queue") %>>
<a href="/docs/providers/aws/r/sqs_queue.html">aws_sqs_queue</a> <a href="/docs/providers/aws/r/sqs_queue.html">aws_sqs_queue</a>
</li> </li>