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_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(),
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
<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>
|
||||||
|
|
Loading…
Reference in New Issue