Merge pull request #2263 from hashicorp/f-aws-spot-instance-request
provider/aws: spot_instance_request
This commit is contained in:
commit
e305d7c5df
|
@ -128,6 +128,7 @@ func Provider() terraform.ResourceProvider {
|
|||
"aws_s3_bucket": resourceAwsS3Bucket(),
|
||||
"aws_security_group": resourceAwsSecurityGroup(),
|
||||
"aws_security_group_rule": resourceAwsSecurityGroupRule(),
|
||||
"aws_spot_instance_request": resourceAwsSpotInstanceRequest(),
|
||||
"aws_sqs_queue": resourceAwsSqsQueue(),
|
||||
"aws_sns_topic": resourceAwsSnsTopic(),
|
||||
"aws_sns_topic_subscription": resourceAwsSnsTopicSubscription(),
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"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/hashcode"
|
||||
"github.com/hashicorp/terraform/helper/resource"
|
||||
|
@ -37,8 +38,9 @@ func resourceAwsInstance() *schema.Resource {
|
|||
|
||||
"associate_public_ip_address": &schema.Schema{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
Default: false,
|
||||
ForceNew: true,
|
||||
Optional: true,
|
||||
},
|
||||
|
||||
"availability_zone": &schema.Schema{
|
||||
|
@ -313,214 +315,32 @@ func resourceAwsInstance() *schema.Resource {
|
|||
func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
|
||||
conn := meta.(*AWSClient).ec2conn
|
||||
|
||||
// Figure out user data
|
||||
userData := ""
|
||||
if v := d.Get("user_data"); v != nil {
|
||||
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)),
|
||||
instanceOpts, err := buildAwsInstanceOpts(d, meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Build the creation struct
|
||||
runOpts := &ec2.RunInstancesInput{
|
||||
ImageID: aws.String(d.Get("ami").(string)),
|
||||
Placement: placement,
|
||||
InstanceType: aws.String(d.Get("instance_type").(string)),
|
||||
BlockDeviceMappings: instanceOpts.BlockDeviceMappings,
|
||||
DisableAPITermination: instanceOpts.DisableAPITermination,
|
||||
EBSOptimized: instanceOpts.EBSOptimized,
|
||||
IAMInstanceProfile: instanceOpts.IAMInstanceProfile,
|
||||
ImageID: instanceOpts.ImageID,
|
||||
InstanceType: instanceOpts.InstanceType,
|
||||
MaxCount: aws.Long(int64(1)),
|
||||
MinCount: aws.Long(int64(1)),
|
||||
UserData: aws.String(userData),
|
||||
EBSOptimized: aws.Boolean(d.Get("ebs_optimized").(bool)),
|
||||
DisableAPITermination: aws.Boolean(d.Get("disable_api_termination").(bool)),
|
||||
IAMInstanceProfile: iam,
|
||||
}
|
||||
|
||||
associatePublicIPAddress := false
|
||||
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
|
||||
NetworkInterfaces: instanceOpts.NetworkInterfaces,
|
||||
Placement: instanceOpts.Placement,
|
||||
PrivateIPAddress: instanceOpts.PrivateIPAddress,
|
||||
SecurityGroupIDs: instanceOpts.SecurityGroupIDs,
|
||||
SecurityGroups: instanceOpts.SecurityGroups,
|
||||
SubnetID: instanceOpts.SubnetID,
|
||||
UserData: instanceOpts.UserData64,
|
||||
}
|
||||
|
||||
// Create the instance
|
||||
log.Printf("[DEBUG] Run configuration: %#v", runOpts)
|
||||
var err error
|
||||
log.Printf("[DEBUG] Run configuration: %s", awsutil.StringValue(runOpts))
|
||||
|
||||
var runResp *ec2.Reservation
|
||||
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 {
|
||||
conn := meta.(*AWSClient).ec2conn
|
||||
|
||||
log.Printf("[INFO] Terminating instance: %s", d.Id())
|
||||
req := &ec2.TerminateInstancesInput{
|
||||
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)
|
||||
if err := awsTerminateInstance(conn, d.Id()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.SetId("")
|
||||
|
@ -926,3 +722,261 @@ func fetchRootDeviceName(ami string, conn *ec2.EC2) (*string, error) {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
`
|
|
@ -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.
|
|
@ -197,6 +197,10 @@
|
|||
<a href="/docs/providers/aws/r/sns_topic_subscription.html">aws_sns_topic_subscription</a>
|
||||
</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") %>>
|
||||
<a href="/docs/providers/aws/r/sqs_queue.html">aws_sqs_queue</a>
|
||||
</li>
|
||||
|
|
Loading…
Reference in New Issue