1687 lines
49 KiB
Go
1687 lines
49 KiB
Go
package aws
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
|
"github.com/aws/aws-sdk-go/service/ec2"
|
|
"github.com/hashicorp/terraform/helper/hashcode"
|
|
"github.com/hashicorp/terraform/helper/resource"
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
)
|
|
|
|
func resourceAwsInstance() *schema.Resource {
|
|
return &schema.Resource{
|
|
Create: resourceAwsInstanceCreate,
|
|
Read: resourceAwsInstanceRead,
|
|
Update: resourceAwsInstanceUpdate,
|
|
Delete: resourceAwsInstanceDelete,
|
|
Importer: &schema.ResourceImporter{
|
|
State: schema.ImportStatePassthrough,
|
|
},
|
|
|
|
SchemaVersion: 1,
|
|
MigrateState: resourceAwsInstanceMigrateState,
|
|
|
|
Timeouts: &schema.ResourceTimeout{
|
|
Create: schema.DefaultTimeout(10 * time.Minute),
|
|
Update: schema.DefaultTimeout(10 * time.Minute),
|
|
Delete: schema.DefaultTimeout(10 * time.Minute),
|
|
},
|
|
|
|
Schema: map[string]*schema.Schema{
|
|
"ami": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"associate_public_ip_address": {
|
|
Type: schema.TypeBool,
|
|
ForceNew: true,
|
|
Computed: true,
|
|
Optional: true,
|
|
},
|
|
|
|
"availability_zone": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"placement_group": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"instance_type": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
|
|
"key_name": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Computed: true,
|
|
},
|
|
|
|
"subnet_id": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"private_ip": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Computed: true,
|
|
},
|
|
|
|
"source_dest_check": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Default: true,
|
|
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
|
|
// Suppress diff if network_interface is set
|
|
_, ok := d.GetOk("network_interface")
|
|
return ok
|
|
},
|
|
},
|
|
|
|
"user_data": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
StateFunc: func(v interface{}) string {
|
|
switch v.(type) {
|
|
case string:
|
|
return userDataHashSum(v.(string))
|
|
default:
|
|
return ""
|
|
}
|
|
},
|
|
},
|
|
|
|
"security_groups": {
|
|
Type: schema.TypeSet,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
Set: schema.HashString,
|
|
},
|
|
|
|
"vpc_security_group_ids": {
|
|
Type: schema.TypeSet,
|
|
Optional: true,
|
|
Computed: true,
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
Set: schema.HashString,
|
|
},
|
|
|
|
"public_dns": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
// TODO: Deprecate me v0.10.0
|
|
"network_interface_id": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
Deprecated: "Please use `primary_network_interface_id` instead",
|
|
},
|
|
|
|
"primary_network_interface_id": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"network_interface": {
|
|
ConflictsWith: []string{"associate_public_ip_address", "subnet_id", "private_ip", "vpc_security_group_ids", "security_groups", "ipv6_addresses", "ipv6_address_count", "source_dest_check"},
|
|
Type: schema.TypeSet,
|
|
Optional: true,
|
|
Computed: true,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"delete_on_termination": {
|
|
Type: schema.TypeBool,
|
|
Default: false,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
"network_interface_id": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
"device_index": {
|
|
Type: schema.TypeInt,
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
"public_ip": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"instance_state": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"private_dns": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"ebs_optimized": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"disable_api_termination": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
},
|
|
|
|
"instance_initiated_shutdown_behavior": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
|
|
"monitoring": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
},
|
|
|
|
"iam_instance_profile": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
|
|
"ipv6_address_count": {
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Computed: true,
|
|
},
|
|
|
|
"ipv6_addresses": {
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
Elem: &schema.Schema{
|
|
Type: schema.TypeString,
|
|
},
|
|
},
|
|
|
|
"tenancy": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"tags": tagsSchema(),
|
|
|
|
"volume_tags": tagsSchemaComputed(),
|
|
|
|
"block_device": {
|
|
Type: schema.TypeMap,
|
|
Optional: true,
|
|
Removed: "Split out into three sub-types; see Changelog and Docs",
|
|
},
|
|
|
|
"ebs_block_device": {
|
|
Type: schema.TypeSet,
|
|
Optional: true,
|
|
Computed: true,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"delete_on_termination": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Default: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"device_name": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"encrypted": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"iops": {
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"snapshot_id": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"volume_size": {
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"volume_type": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
},
|
|
},
|
|
Set: func(v interface{}) int {
|
|
var buf bytes.Buffer
|
|
m := v.(map[string]interface{})
|
|
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
|
|
buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string)))
|
|
return hashcode.String(buf.String())
|
|
},
|
|
},
|
|
|
|
"ephemeral_block_device": {
|
|
Type: schema.TypeSet,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"device_name": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
|
|
"virtual_name": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
|
|
"no_device": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
Set: func(v interface{}) int {
|
|
var buf bytes.Buffer
|
|
m := v.(map[string]interface{})
|
|
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
|
|
buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string)))
|
|
if v, ok := m["no_device"].(bool); ok && v {
|
|
buf.WriteString(fmt.Sprintf("%t-", v))
|
|
}
|
|
return hashcode.String(buf.String())
|
|
},
|
|
},
|
|
|
|
"root_block_device": {
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
Computed: true,
|
|
MaxItems: 1,
|
|
Elem: &schema.Resource{
|
|
// "You can only modify the volume size, volume type, and Delete on
|
|
// Termination flag on the block device mapping entry for the root
|
|
// device volume." - bit.ly/ec2bdmap
|
|
Schema: map[string]*schema.Schema{
|
|
"delete_on_termination": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Default: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"iops": {
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"volume_size": {
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"volume_type": {
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
|
|
conn := meta.(*AWSClient).ec2conn
|
|
|
|
instanceOpts, err := buildAwsInstanceOpts(d, meta)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Build the creation struct
|
|
runOpts := &ec2.RunInstancesInput{
|
|
BlockDeviceMappings: instanceOpts.BlockDeviceMappings,
|
|
DisableApiTermination: instanceOpts.DisableAPITermination,
|
|
EbsOptimized: instanceOpts.EBSOptimized,
|
|
Monitoring: instanceOpts.Monitoring,
|
|
IamInstanceProfile: instanceOpts.IAMInstanceProfile,
|
|
ImageId: instanceOpts.ImageID,
|
|
InstanceInitiatedShutdownBehavior: instanceOpts.InstanceInitiatedShutdownBehavior,
|
|
InstanceType: instanceOpts.InstanceType,
|
|
Ipv6AddressCount: instanceOpts.Ipv6AddressCount,
|
|
Ipv6Addresses: instanceOpts.Ipv6Addresses,
|
|
KeyName: instanceOpts.KeyName,
|
|
MaxCount: aws.Int64(int64(1)),
|
|
MinCount: aws.Int64(int64(1)),
|
|
NetworkInterfaces: instanceOpts.NetworkInterfaces,
|
|
Placement: instanceOpts.Placement,
|
|
PrivateIpAddress: instanceOpts.PrivateIPAddress,
|
|
SecurityGroupIds: instanceOpts.SecurityGroupIDs,
|
|
SecurityGroups: instanceOpts.SecurityGroups,
|
|
SubnetId: instanceOpts.SubnetID,
|
|
UserData: instanceOpts.UserData64,
|
|
}
|
|
|
|
_, ipv6CountOk := d.GetOk("ipv6_address_count")
|
|
_, ipv6AddressOk := d.GetOk("ipv6_addresses")
|
|
|
|
if ipv6AddressOk && ipv6CountOk {
|
|
return fmt.Errorf("Only 1 of `ipv6_address_count` or `ipv6_addresses` can be specified")
|
|
}
|
|
|
|
restricted := meta.(*AWSClient).IsGovCloud() || meta.(*AWSClient).IsChinaCloud()
|
|
if !restricted {
|
|
tagsSpec := make([]*ec2.TagSpecification, 0)
|
|
|
|
if v, ok := d.GetOk("tags"); ok {
|
|
tags := tagsFromMap(v.(map[string]interface{}))
|
|
|
|
spec := &ec2.TagSpecification{
|
|
ResourceType: aws.String("instance"),
|
|
Tags: tags,
|
|
}
|
|
|
|
tagsSpec = append(tagsSpec, spec)
|
|
}
|
|
|
|
if v, ok := d.GetOk("volume_tags"); ok {
|
|
tags := tagsFromMap(v.(map[string]interface{}))
|
|
|
|
spec := &ec2.TagSpecification{
|
|
ResourceType: aws.String("volume"),
|
|
Tags: tags,
|
|
}
|
|
|
|
tagsSpec = append(tagsSpec, spec)
|
|
}
|
|
|
|
if len(tagsSpec) > 0 {
|
|
runOpts.TagSpecifications = tagsSpec
|
|
}
|
|
}
|
|
|
|
// Create the instance
|
|
log.Printf("[DEBUG] Run configuration: %s", runOpts)
|
|
|
|
var runResp *ec2.Reservation
|
|
err = resource.Retry(15*time.Second, func() *resource.RetryError {
|
|
var err error
|
|
runResp, err = conn.RunInstances(runOpts)
|
|
// IAM instance profiles can take ~10 seconds to propagate in AWS:
|
|
// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console
|
|
if isAWSErr(err, "InvalidParameterValue", "Invalid IAM Instance Profile") {
|
|
log.Print("[DEBUG] Invalid IAM Instance Profile referenced, retrying...")
|
|
return resource.RetryableError(err)
|
|
}
|
|
// IAM roles can also take time to propagate in AWS:
|
|
if isAWSErr(err, "InvalidParameterValue", " has no associated IAM Roles") {
|
|
log.Print("[DEBUG] IAM Instance Profile appears to have no IAM roles, retrying...")
|
|
return resource.RetryableError(err)
|
|
}
|
|
return resource.NonRetryableError(err)
|
|
})
|
|
// Warn if the AWS Error involves group ids, to help identify situation
|
|
// where a user uses group ids in security_groups for the Default VPC.
|
|
// See https://github.com/hashicorp/terraform/issues/3798
|
|
if isAWSErr(err, "InvalidParameterValue", "groupId is invalid") {
|
|
return fmt.Errorf("Error launching instance, possible mismatch of Security Group IDs and Names. See AWS Instance docs here: %s.\n\n\tAWS Error: %s", "https://terraform.io/docs/providers/aws/r/instance.html", err.(awserr.Error).Message())
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("Error launching source instance: %s", err)
|
|
}
|
|
if runResp == nil || len(runResp.Instances) == 0 {
|
|
return errors.New("Error launching source instance: no instances returned in response")
|
|
}
|
|
|
|
instance := runResp.Instances[0]
|
|
log.Printf("[INFO] Instance ID: %s", *instance.InstanceId)
|
|
|
|
// Store the resulting ID so we can look this up later
|
|
d.SetId(*instance.InstanceId)
|
|
|
|
// Wait for the instance to become running so we can get some attributes
|
|
// that aren't available until later.
|
|
log.Printf(
|
|
"[DEBUG] Waiting for instance (%s) to become running",
|
|
*instance.InstanceId)
|
|
|
|
stateConf := &resource.StateChangeConf{
|
|
Pending: []string{"pending"},
|
|
Target: []string{"running"},
|
|
Refresh: InstanceStateRefreshFunc(conn, *instance.InstanceId, "terminated"),
|
|
Timeout: d.Timeout(schema.TimeoutCreate),
|
|
Delay: 10 * time.Second,
|
|
MinTimeout: 3 * time.Second,
|
|
}
|
|
|
|
instanceRaw, err := stateConf.WaitForState()
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"Error waiting for instance (%s) to become ready: %s",
|
|
*instance.InstanceId, err)
|
|
}
|
|
|
|
instance = instanceRaw.(*ec2.Instance)
|
|
|
|
// Initialize the connection info
|
|
if instance.PublicIpAddress != nil {
|
|
d.SetConnInfo(map[string]string{
|
|
"type": "ssh",
|
|
"host": *instance.PublicIpAddress,
|
|
})
|
|
} else if instance.PrivateIpAddress != nil {
|
|
d.SetConnInfo(map[string]string{
|
|
"type": "ssh",
|
|
"host": *instance.PrivateIpAddress,
|
|
})
|
|
}
|
|
|
|
// Update if we need to
|
|
return resourceAwsInstanceUpdate(d, meta)
|
|
}
|
|
|
|
func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
|
conn := meta.(*AWSClient).ec2conn
|
|
|
|
resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
|
|
InstanceIds: []*string{aws.String(d.Id())},
|
|
})
|
|
if err != nil {
|
|
// If the instance was not found, return nil so that we can show
|
|
// that the instance is gone.
|
|
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" {
|
|
d.SetId("")
|
|
return nil
|
|
}
|
|
|
|
// Some other error, report it
|
|
return err
|
|
}
|
|
|
|
// If nothing was found, then return no state
|
|
if len(resp.Reservations) == 0 {
|
|
d.SetId("")
|
|
return nil
|
|
}
|
|
|
|
instance := resp.Reservations[0].Instances[0]
|
|
|
|
if instance.State != nil {
|
|
// If the instance is terminated, then it is gone
|
|
if *instance.State.Name == "terminated" {
|
|
d.SetId("")
|
|
return nil
|
|
}
|
|
|
|
d.Set("instance_state", instance.State.Name)
|
|
}
|
|
|
|
if instance.Placement != nil {
|
|
d.Set("availability_zone", instance.Placement.AvailabilityZone)
|
|
}
|
|
if instance.Placement.Tenancy != nil {
|
|
d.Set("tenancy", instance.Placement.Tenancy)
|
|
}
|
|
|
|
d.Set("ami", instance.ImageId)
|
|
d.Set("instance_type", instance.InstanceType)
|
|
d.Set("key_name", instance.KeyName)
|
|
d.Set("public_dns", instance.PublicDnsName)
|
|
d.Set("public_ip", instance.PublicIpAddress)
|
|
d.Set("private_dns", instance.PrivateDnsName)
|
|
d.Set("private_ip", instance.PrivateIpAddress)
|
|
d.Set("iam_instance_profile", iamInstanceProfileArnToName(instance.IamInstanceProfile))
|
|
|
|
// Set configured Network Interface Device Index Slice
|
|
// We only want to read, and populate state for the configured network_interface attachments. Otherwise, other
|
|
// resources have the potential to attach network interfaces to the instance, and cause a perpetual create/destroy
|
|
// diff. We should only read on changes configured for this specific resource because of this.
|
|
var configuredDeviceIndexes []int
|
|
if v, ok := d.GetOk("network_interface"); ok {
|
|
vL := v.(*schema.Set).List()
|
|
for _, vi := range vL {
|
|
mVi := vi.(map[string]interface{})
|
|
configuredDeviceIndexes = append(configuredDeviceIndexes, mVi["device_index"].(int))
|
|
}
|
|
}
|
|
|
|
var ipv6Addresses []string
|
|
if len(instance.NetworkInterfaces) > 0 {
|
|
var primaryNetworkInterface ec2.InstanceNetworkInterface
|
|
var networkInterfaces []map[string]interface{}
|
|
for _, iNi := range instance.NetworkInterfaces {
|
|
ni := make(map[string]interface{})
|
|
if *iNi.Attachment.DeviceIndex == 0 {
|
|
primaryNetworkInterface = *iNi
|
|
}
|
|
// If the attached network device is inside our configuration, refresh state with values found.
|
|
// Otherwise, assume the network device was attached via an outside resource.
|
|
for _, index := range configuredDeviceIndexes {
|
|
if index == int(*iNi.Attachment.DeviceIndex) {
|
|
ni["device_index"] = *iNi.Attachment.DeviceIndex
|
|
ni["network_interface_id"] = *iNi.NetworkInterfaceId
|
|
ni["delete_on_termination"] = *iNi.Attachment.DeleteOnTermination
|
|
}
|
|
}
|
|
// Don't add empty network interfaces to schema
|
|
if len(ni) == 0 {
|
|
continue
|
|
}
|
|
networkInterfaces = append(networkInterfaces, ni)
|
|
}
|
|
if err := d.Set("network_interface", networkInterfaces); err != nil {
|
|
return fmt.Errorf("Error setting network_interfaces: %v", err)
|
|
}
|
|
|
|
// Set primary network interface details
|
|
// If an instance is shutting down, network interfaces are detached, and attributes may be nil,
|
|
// need to protect against nil pointer dereferences
|
|
if primaryNetworkInterface.SubnetId != nil {
|
|
d.Set("subnet_id", primaryNetworkInterface.SubnetId)
|
|
}
|
|
if primaryNetworkInterface.NetworkInterfaceId != nil {
|
|
d.Set("network_interface_id", primaryNetworkInterface.NetworkInterfaceId) // TODO: Deprecate me v0.10.0
|
|
d.Set("primary_network_interface_id", primaryNetworkInterface.NetworkInterfaceId)
|
|
}
|
|
if primaryNetworkInterface.Ipv6Addresses != nil {
|
|
d.Set("ipv6_address_count", len(primaryNetworkInterface.Ipv6Addresses))
|
|
}
|
|
if primaryNetworkInterface.SourceDestCheck != nil {
|
|
d.Set("source_dest_check", primaryNetworkInterface.SourceDestCheck)
|
|
}
|
|
|
|
d.Set("associate_public_ip_address", primaryNetworkInterface.Association != nil)
|
|
|
|
for _, address := range primaryNetworkInterface.Ipv6Addresses {
|
|
ipv6Addresses = append(ipv6Addresses, *address.Ipv6Address)
|
|
}
|
|
|
|
} else {
|
|
d.Set("subnet_id", instance.SubnetId)
|
|
d.Set("network_interface_id", "") // TODO: Deprecate me v0.10.0
|
|
d.Set("primary_network_interface_id", "")
|
|
}
|
|
|
|
if err := d.Set("ipv6_addresses", ipv6Addresses); err != nil {
|
|
log.Printf("[WARN] Error setting ipv6_addresses for AWS Instance (%s): %s", d.Id(), err)
|
|
}
|
|
|
|
d.Set("ebs_optimized", instance.EbsOptimized)
|
|
if instance.SubnetId != nil && *instance.SubnetId != "" {
|
|
d.Set("source_dest_check", instance.SourceDestCheck)
|
|
}
|
|
|
|
if instance.Monitoring != nil && instance.Monitoring.State != nil {
|
|
monitoringState := *instance.Monitoring.State
|
|
d.Set("monitoring", monitoringState == "enabled" || monitoringState == "pending")
|
|
}
|
|
|
|
d.Set("tags", tagsToMap(instance.Tags))
|
|
|
|
if err := readVolumeTags(conn, d); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := readSecurityGroups(d, instance); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := readBlockDevices(d, instance, conn); err != nil {
|
|
return err
|
|
}
|
|
if _, ok := d.GetOk("ephemeral_block_device"); !ok {
|
|
d.Set("ephemeral_block_device", []interface{}{})
|
|
}
|
|
|
|
// Instance attributes
|
|
{
|
|
attr, err := conn.DescribeInstanceAttribute(&ec2.DescribeInstanceAttributeInput{
|
|
Attribute: aws.String("disableApiTermination"),
|
|
InstanceId: aws.String(d.Id()),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.Set("disable_api_termination", attr.DisableApiTermination.Value)
|
|
}
|
|
{
|
|
attr, err := conn.DescribeInstanceAttribute(&ec2.DescribeInstanceAttributeInput{
|
|
Attribute: aws.String(ec2.InstanceAttributeNameUserData),
|
|
InstanceId: aws.String(d.Id()),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if attr.UserData.Value != nil {
|
|
d.Set("user_data", userDataHashSum(*attr.UserData.Value))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
|
|
conn := meta.(*AWSClient).ec2conn
|
|
|
|
d.Partial(true)
|
|
|
|
restricted := meta.(*AWSClient).IsGovCloud() || meta.(*AWSClient).IsChinaCloud()
|
|
|
|
if d.HasChange("tags") {
|
|
if !d.IsNewResource() || restricted {
|
|
if err := setTags(conn, d); err != nil {
|
|
return err
|
|
} else {
|
|
d.SetPartial("tags")
|
|
}
|
|
}
|
|
}
|
|
if d.HasChange("volume_tags") {
|
|
if !d.IsNewResource() || !restricted {
|
|
if err := setVolumeTags(conn, d); err != nil {
|
|
return err
|
|
} else {
|
|
d.SetPartial("volume_tags")
|
|
}
|
|
}
|
|
}
|
|
|
|
if d.HasChange("iam_instance_profile") && !d.IsNewResource() {
|
|
request := &ec2.DescribeIamInstanceProfileAssociationsInput{
|
|
Filters: []*ec2.Filter{
|
|
{
|
|
Name: aws.String("instance-id"),
|
|
Values: []*string{aws.String(d.Id())},
|
|
},
|
|
},
|
|
}
|
|
|
|
resp, err := conn.DescribeIamInstanceProfileAssociations(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// An Iam Instance Profile has been provided and is pending a change
|
|
// This means it is an association or a replacement to an association
|
|
if _, ok := d.GetOk("iam_instance_profile"); ok {
|
|
// Does not have an Iam Instance Profile associated with it, need to associate
|
|
if len(resp.IamInstanceProfileAssociations) == 0 {
|
|
_, err := conn.AssociateIamInstanceProfile(&ec2.AssociateIamInstanceProfileInput{
|
|
InstanceId: aws.String(d.Id()),
|
|
IamInstanceProfile: &ec2.IamInstanceProfileSpecification{
|
|
Name: aws.String(d.Get("iam_instance_profile").(string)),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
} else {
|
|
// Has an Iam Instance Profile associated with it, need to replace the association
|
|
associationId := resp.IamInstanceProfileAssociations[0].AssociationId
|
|
|
|
_, err := conn.ReplaceIamInstanceProfileAssociation(&ec2.ReplaceIamInstanceProfileAssociationInput{
|
|
AssociationId: associationId,
|
|
IamInstanceProfile: &ec2.IamInstanceProfileSpecification{
|
|
Name: aws.String(d.Get("iam_instance_profile").(string)),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// An Iam Instance Profile has _not_ been provided but is pending a change. This means there is a pending removal
|
|
} else {
|
|
if len(resp.IamInstanceProfileAssociations) > 0 {
|
|
// Has an Iam Instance Profile associated with it, need to remove the association
|
|
associationId := resp.IamInstanceProfileAssociations[0].AssociationId
|
|
|
|
_, err := conn.DisassociateIamInstanceProfile(&ec2.DisassociateIamInstanceProfileInput{
|
|
AssociationId: associationId,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// SourceDestCheck can only be modified on an instance without manually specified network interfaces.
|
|
// SourceDestCheck, in that case, is configured at the network interface level
|
|
if _, ok := d.GetOk("network_interface"); !ok {
|
|
if d.HasChange("source_dest_check") || d.IsNewResource() {
|
|
// SourceDestCheck can only be set on VPC instances // AWS will return an error of InvalidParameterCombination if we attempt
|
|
// to modify the source_dest_check of an instance in EC2 Classic
|
|
log.Printf("[INFO] Modifying `source_dest_check` on Instance %s", d.Id())
|
|
_, err := conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeInput{
|
|
InstanceId: aws.String(d.Id()),
|
|
SourceDestCheck: &ec2.AttributeBooleanValue{
|
|
Value: aws.Bool(d.Get("source_dest_check").(bool)),
|
|
},
|
|
})
|
|
if err != nil {
|
|
if ec2err, ok := err.(awserr.Error); ok {
|
|
// Tolerate InvalidParameterCombination error in Classic, otherwise
|
|
// return the error
|
|
if "InvalidParameterCombination" != ec2err.Code() {
|
|
return err
|
|
}
|
|
log.Printf("[WARN] Attempted to modify SourceDestCheck on non VPC instance: %s", ec2err.Message())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if d.HasChange("vpc_security_group_ids") {
|
|
var groups []*string
|
|
if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 {
|
|
for _, v := range v.List() {
|
|
groups = append(groups, aws.String(v.(string)))
|
|
}
|
|
}
|
|
// If a user has multiple network interface attachments on the target EC2 instance, simply modifying the
|
|
// instance attributes via a `ModifyInstanceAttributes()` request would fail with the following error message:
|
|
// "There are multiple interfaces attached to instance 'i-XX'. Please specify an interface ID for the operation instead."
|
|
// Thus, we need to actually modify the primary network interface for the new security groups, as the primary
|
|
// network interface is where we modify/create security group assignments during Create.
|
|
log.Printf("[INFO] Modifying `vpc_security_group_ids` on Instance %q", d.Id())
|
|
instances, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
|
|
InstanceIds: []*string{aws.String(d.Id())},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
instance := instances.Reservations[0].Instances[0]
|
|
var primaryInterface ec2.InstanceNetworkInterface
|
|
for _, ni := range instance.NetworkInterfaces {
|
|
if *ni.Attachment.DeviceIndex == 0 {
|
|
primaryInterface = *ni
|
|
}
|
|
}
|
|
|
|
if primaryInterface.NetworkInterfaceId == nil {
|
|
log.Print("[Error] Attempted to set vpc_security_group_ids on an instance without a primary network interface")
|
|
return fmt.Errorf(
|
|
"Failed to update vpc_security_group_ids on %q, which does not contain a primary network interface",
|
|
d.Id())
|
|
}
|
|
|
|
if _, err := conn.ModifyNetworkInterfaceAttribute(&ec2.ModifyNetworkInterfaceAttributeInput{
|
|
NetworkInterfaceId: primaryInterface.NetworkInterfaceId,
|
|
Groups: groups,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if d.HasChange("instance_type") && !d.IsNewResource() {
|
|
log.Printf("[INFO] Stopping Instance %q for instance_type change", d.Id())
|
|
_, err := conn.StopInstances(&ec2.StopInstancesInput{
|
|
InstanceIds: []*string{aws.String(d.Id())},
|
|
})
|
|
|
|
stateConf := &resource.StateChangeConf{
|
|
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
|
|
Target: []string{"stopped"},
|
|
Refresh: InstanceStateRefreshFunc(conn, d.Id(), ""),
|
|
Timeout: d.Timeout(schema.TimeoutUpdate),
|
|
Delay: 10 * time.Second,
|
|
MinTimeout: 3 * time.Second,
|
|
}
|
|
|
|
_, err = stateConf.WaitForState()
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"Error waiting for instance (%s) to stop: %s", d.Id(), err)
|
|
}
|
|
|
|
log.Printf("[INFO] Modifying instance type %s", d.Id())
|
|
_, err = conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeInput{
|
|
InstanceId: aws.String(d.Id()),
|
|
InstanceType: &ec2.AttributeValue{
|
|
Value: aws.String(d.Get("instance_type").(string)),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Printf("[INFO] Starting Instance %q after instance_type change", d.Id())
|
|
_, err = conn.StartInstances(&ec2.StartInstancesInput{
|
|
InstanceIds: []*string{aws.String(d.Id())},
|
|
})
|
|
|
|
stateConf = &resource.StateChangeConf{
|
|
Pending: []string{"pending", "stopped"},
|
|
Target: []string{"running"},
|
|
Refresh: InstanceStateRefreshFunc(conn, d.Id(), "terminated"),
|
|
Timeout: d.Timeout(schema.TimeoutUpdate),
|
|
Delay: 10 * time.Second,
|
|
MinTimeout: 3 * time.Second,
|
|
}
|
|
|
|
_, err = stateConf.WaitForState()
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"Error waiting for instance (%s) to become ready: %s",
|
|
d.Id(), err)
|
|
}
|
|
}
|
|
|
|
if d.HasChange("disable_api_termination") {
|
|
_, err := conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeInput{
|
|
InstanceId: aws.String(d.Id()),
|
|
DisableApiTermination: &ec2.AttributeBooleanValue{
|
|
Value: aws.Bool(d.Get("disable_api_termination").(bool)),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if d.HasChange("instance_initiated_shutdown_behavior") {
|
|
log.Printf("[INFO] Modifying instance %s", d.Id())
|
|
_, err := conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeInput{
|
|
InstanceId: aws.String(d.Id()),
|
|
InstanceInitiatedShutdownBehavior: &ec2.AttributeValue{
|
|
Value: aws.String(d.Get("instance_initiated_shutdown_behavior").(string)),
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if d.HasChange("monitoring") {
|
|
var mErr error
|
|
if d.Get("monitoring").(bool) {
|
|
log.Printf("[DEBUG] Enabling monitoring for Instance (%s)", d.Id())
|
|
_, mErr = conn.MonitorInstances(&ec2.MonitorInstancesInput{
|
|
InstanceIds: []*string{aws.String(d.Id())},
|
|
})
|
|
} else {
|
|
log.Printf("[DEBUG] Disabling monitoring for Instance (%s)", d.Id())
|
|
_, mErr = conn.UnmonitorInstances(&ec2.UnmonitorInstancesInput{
|
|
InstanceIds: []*string{aws.String(d.Id())},
|
|
})
|
|
}
|
|
if mErr != nil {
|
|
return fmt.Errorf("[WARN] Error updating Instance monitoring: %s", mErr)
|
|
}
|
|
}
|
|
|
|
// TODO(mitchellh): wait for the attributes we modified to
|
|
// persist the change...
|
|
|
|
d.Partial(false)
|
|
|
|
return resourceAwsInstanceRead(d, meta)
|
|
}
|
|
|
|
func resourceAwsInstanceDelete(d *schema.ResourceData, meta interface{}) error {
|
|
conn := meta.(*AWSClient).ec2conn
|
|
|
|
if err := awsTerminateInstance(conn, d.Id(), d); err != nil {
|
|
return err
|
|
}
|
|
|
|
d.SetId("")
|
|
return nil
|
|
}
|
|
|
|
// InstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
|
|
// an EC2 instance.
|
|
func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID, failState string) resource.StateRefreshFunc {
|
|
return func() (interface{}, string, error) {
|
|
resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
|
|
InstanceIds: []*string{aws.String(instanceID)},
|
|
})
|
|
if err != nil {
|
|
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" {
|
|
// Set this to nil as if we didn't find anything.
|
|
resp = nil
|
|
} else {
|
|
log.Printf("Error on InstanceStateRefresh: %s", err)
|
|
return nil, "", err
|
|
}
|
|
}
|
|
|
|
if resp == nil || len(resp.Reservations) == 0 || len(resp.Reservations[0].Instances) == 0 {
|
|
// Sometimes AWS just has consistency issues and doesn't see
|
|
// our instance yet. Return an empty state.
|
|
return nil, "", nil
|
|
}
|
|
|
|
i := resp.Reservations[0].Instances[0]
|
|
state := *i.State.Name
|
|
|
|
if state == failState {
|
|
return i, state, fmt.Errorf("Failed to reach target state. Reason: %s",
|
|
stringifyStateReason(i.StateReason))
|
|
|
|
}
|
|
|
|
return i, state, nil
|
|
}
|
|
}
|
|
|
|
func stringifyStateReason(sr *ec2.StateReason) string {
|
|
if sr.Message != nil {
|
|
return *sr.Message
|
|
}
|
|
if sr.Code != nil {
|
|
return *sr.Code
|
|
}
|
|
|
|
return sr.String()
|
|
}
|
|
|
|
func readBlockDevices(d *schema.ResourceData, instance *ec2.Instance, conn *ec2.EC2) error {
|
|
ibds, err := readBlockDevicesFromInstance(instance, conn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := d.Set("ebs_block_device", ibds["ebs"]); err != nil {
|
|
return err
|
|
}
|
|
|
|
// This handles the import case which needs to be defaulted to empty
|
|
if _, ok := d.GetOk("root_block_device"); !ok {
|
|
if err := d.Set("root_block_device", []interface{}{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if ibds["root"] != nil {
|
|
roots := []interface{}{ibds["root"]}
|
|
if err := d.Set("root_block_device", roots); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func readBlockDevicesFromInstance(instance *ec2.Instance, conn *ec2.EC2) (map[string]interface{}, error) {
|
|
blockDevices := make(map[string]interface{})
|
|
blockDevices["ebs"] = make([]map[string]interface{}, 0)
|
|
blockDevices["root"] = nil
|
|
|
|
instanceBlockDevices := make(map[string]*ec2.InstanceBlockDeviceMapping)
|
|
for _, bd := range instance.BlockDeviceMappings {
|
|
if bd.Ebs != nil {
|
|
instanceBlockDevices[*bd.Ebs.VolumeId] = bd
|
|
}
|
|
}
|
|
|
|
if len(instanceBlockDevices) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
volIDs := make([]*string, 0, len(instanceBlockDevices))
|
|
for volID := range instanceBlockDevices {
|
|
volIDs = append(volIDs, aws.String(volID))
|
|
}
|
|
|
|
// Need to call DescribeVolumes to get volume_size and volume_type for each
|
|
// EBS block device
|
|
volResp, err := conn.DescribeVolumes(&ec2.DescribeVolumesInput{
|
|
VolumeIds: volIDs,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, vol := range volResp.Volumes {
|
|
instanceBd := instanceBlockDevices[*vol.VolumeId]
|
|
bd := make(map[string]interface{})
|
|
|
|
if instanceBd.Ebs != nil && instanceBd.Ebs.DeleteOnTermination != nil {
|
|
bd["delete_on_termination"] = *instanceBd.Ebs.DeleteOnTermination
|
|
}
|
|
if vol.Size != nil {
|
|
bd["volume_size"] = *vol.Size
|
|
}
|
|
if vol.VolumeType != nil {
|
|
bd["volume_type"] = *vol.VolumeType
|
|
}
|
|
if vol.Iops != nil {
|
|
bd["iops"] = *vol.Iops
|
|
}
|
|
|
|
if blockDeviceIsRoot(instanceBd, instance) {
|
|
blockDevices["root"] = bd
|
|
} else {
|
|
if instanceBd.DeviceName != nil {
|
|
bd["device_name"] = *instanceBd.DeviceName
|
|
}
|
|
if vol.Encrypted != nil {
|
|
bd["encrypted"] = *vol.Encrypted
|
|
}
|
|
if vol.SnapshotId != nil {
|
|
bd["snapshot_id"] = *vol.SnapshotId
|
|
}
|
|
|
|
blockDevices["ebs"] = append(blockDevices["ebs"].([]map[string]interface{}), bd)
|
|
}
|
|
}
|
|
|
|
return blockDevices, nil
|
|
}
|
|
|
|
func blockDeviceIsRoot(bd *ec2.InstanceBlockDeviceMapping, instance *ec2.Instance) bool {
|
|
return bd.DeviceName != nil &&
|
|
instance.RootDeviceName != nil &&
|
|
*bd.DeviceName == *instance.RootDeviceName
|
|
}
|
|
|
|
func fetchRootDeviceName(ami string, conn *ec2.EC2) (*string, error) {
|
|
if ami == "" {
|
|
return nil, errors.New("Cannot fetch root device name for blank AMI ID.")
|
|
}
|
|
|
|
log.Printf("[DEBUG] Describing AMI %q to get root block device name", ami)
|
|
res, err := conn.DescribeImages(&ec2.DescribeImagesInput{
|
|
ImageIds: []*string{aws.String(ami)},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// For a bad image, we just return nil so we don't block a refresh
|
|
if len(res.Images) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
image := res.Images[0]
|
|
rootDeviceName := image.RootDeviceName
|
|
|
|
// Instance store backed AMIs do not provide a root device name.
|
|
if *image.RootDeviceType == ec2.DeviceTypeInstanceStore {
|
|
return nil, nil
|
|
}
|
|
|
|
// Some AMIs have a RootDeviceName like "/dev/sda1" that does not appear as a
|
|
// DeviceName in the BlockDeviceMapping list (which will instead have
|
|
// something like "/dev/sda")
|
|
//
|
|
// While this seems like it breaks an invariant of AMIs, it ends up working
|
|
// on the AWS side, and AMIs like this are common enough that we need to
|
|
// special case it so Terraform does the right thing.
|
|
//
|
|
// Our heuristic is: if the RootDeviceName does not appear in the
|
|
// BlockDeviceMapping, assume that the DeviceName of the first
|
|
// BlockDeviceMapping entry serves as the root device.
|
|
rootDeviceNameInMapping := false
|
|
for _, bdm := range image.BlockDeviceMappings {
|
|
if bdm.DeviceName == image.RootDeviceName {
|
|
rootDeviceNameInMapping = true
|
|
}
|
|
}
|
|
|
|
if !rootDeviceNameInMapping && len(image.BlockDeviceMappings) > 0 {
|
|
rootDeviceName = image.BlockDeviceMappings[0].DeviceName
|
|
}
|
|
|
|
if rootDeviceName == nil {
|
|
return nil, fmt.Errorf("[WARN] Error finding Root Device Name for AMI (%s)", ami)
|
|
}
|
|
|
|
return rootDeviceName, nil
|
|
}
|
|
|
|
func buildNetworkInterfaceOpts(d *schema.ResourceData, groups []*string, nInterfaces interface{}) []*ec2.InstanceNetworkInterfaceSpecification {
|
|
networkInterfaces := []*ec2.InstanceNetworkInterfaceSpecification{}
|
|
// Get necessary items
|
|
associatePublicIPAddress := d.Get("associate_public_ip_address").(bool)
|
|
subnet, hasSubnet := d.GetOk("subnet_id")
|
|
|
|
if hasSubnet && associatePublicIPAddress {
|
|
// If we have a non-default VPC / Subnet specified, we can flag
|
|
// AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided.
|
|
// You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise
|
|
// you get: Network interfaces and an instance-level subnet ID may not be specified on the same request
|
|
// You also need to attach Security Groups to the NetworkInterface instead of the instance,
|
|
// to avoid: Network interfaces and an instance-level security groups may not be specified on
|
|
// the same request
|
|
ni := &ec2.InstanceNetworkInterfaceSpecification{
|
|
AssociatePublicIpAddress: aws.Bool(associatePublicIPAddress),
|
|
DeviceIndex: aws.Int64(int64(0)),
|
|
SubnetId: aws.String(subnet.(string)),
|
|
Groups: groups,
|
|
}
|
|
|
|
if v, ok := d.GetOk("private_ip"); ok {
|
|
ni.PrivateIpAddress = aws.String(v.(string))
|
|
}
|
|
|
|
if v, ok := d.GetOk("ipv6_address_count"); ok {
|
|
ni.Ipv6AddressCount = aws.Int64(int64(v.(int)))
|
|
}
|
|
|
|
if v, ok := d.GetOk("ipv6_addresses"); ok {
|
|
ipv6Addresses := make([]*ec2.InstanceIpv6Address, len(v.([]interface{})))
|
|
for _, address := range v.([]interface{}) {
|
|
ipv6Address := &ec2.InstanceIpv6Address{
|
|
Ipv6Address: aws.String(address.(string)),
|
|
}
|
|
|
|
ipv6Addresses = append(ipv6Addresses, ipv6Address)
|
|
}
|
|
|
|
ni.Ipv6Addresses = ipv6Addresses
|
|
}
|
|
|
|
if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 {
|
|
for _, v := range v.List() {
|
|
ni.Groups = append(ni.Groups, aws.String(v.(string)))
|
|
}
|
|
}
|
|
|
|
networkInterfaces = append(networkInterfaces, ni)
|
|
} else {
|
|
// If we have manually specified network interfaces, build and attach those here.
|
|
vL := nInterfaces.(*schema.Set).List()
|
|
for _, v := range vL {
|
|
ini := v.(map[string]interface{})
|
|
ni := &ec2.InstanceNetworkInterfaceSpecification{
|
|
DeviceIndex: aws.Int64(int64(ini["device_index"].(int))),
|
|
NetworkInterfaceId: aws.String(ini["network_interface_id"].(string)),
|
|
DeleteOnTermination: aws.Bool(ini["delete_on_termination"].(bool)),
|
|
}
|
|
networkInterfaces = append(networkInterfaces, ni)
|
|
}
|
|
}
|
|
|
|
return networkInterfaces
|
|
}
|
|
|
|
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.Bool(bd["delete_on_termination"].(bool)),
|
|
}
|
|
|
|
if v, ok := bd["snapshot_id"].(string); ok && v != "" {
|
|
ebs.SnapshotId = aws.String(v)
|
|
}
|
|
|
|
if v, ok := bd["encrypted"].(bool); ok && v {
|
|
ebs.Encrypted = aws.Bool(v)
|
|
}
|
|
|
|
if v, ok := bd["volume_size"].(int); ok && v != 0 {
|
|
ebs.VolumeSize = aws.Int64(int64(v))
|
|
}
|
|
|
|
if v, ok := bd["volume_type"].(string); ok && v != "" {
|
|
ebs.VolumeType = aws.String(v)
|
|
if "io1" == strings.ToLower(v) {
|
|
// Condition: This parameter is required for requests to create io1
|
|
// volumes; it is not used in requests to create gp2, st1, sc1, or
|
|
// standard volumes.
|
|
// See: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_EbsBlockDevice.html
|
|
if v, ok := bd["iops"].(int); ok && v > 0 {
|
|
ebs.Iops = aws.Int64(int64(v))
|
|
}
|
|
}
|
|
}
|
|
|
|
blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{
|
|
DeviceName: aws.String(bd["device_name"].(string)),
|
|
Ebs: ebs,
|
|
})
|
|
}
|
|
}
|
|
|
|
if v, ok := d.GetOk("ephemeral_block_device"); ok {
|
|
vL := v.(*schema.Set).List()
|
|
for _, v := range vL {
|
|
bd := v.(map[string]interface{})
|
|
bdm := &ec2.BlockDeviceMapping{
|
|
DeviceName: aws.String(bd["device_name"].(string)),
|
|
VirtualName: aws.String(bd["virtual_name"].(string)),
|
|
}
|
|
if v, ok := bd["no_device"].(bool); ok && v {
|
|
bdm.NoDevice = aws.String("")
|
|
// When NoDevice is true, just ignore VirtualName since it's not needed
|
|
bdm.VirtualName = nil
|
|
}
|
|
|
|
if bdm.NoDevice == nil && aws.StringValue(bdm.VirtualName) == "" {
|
|
return nil, errors.New("virtual_name cannot be empty when no_device is false or undefined.")
|
|
}
|
|
|
|
blockDevices = append(blockDevices, bdm)
|
|
}
|
|
}
|
|
|
|
if v, ok := d.GetOk("root_block_device"); ok {
|
|
vL := v.([]interface{})
|
|
if len(vL) > 1 {
|
|
return nil, errors.New("Cannot specify more than one root_block_device.")
|
|
}
|
|
for _, v := range vL {
|
|
bd := v.(map[string]interface{})
|
|
ebs := &ec2.EbsBlockDevice{
|
|
DeleteOnTermination: aws.Bool(bd["delete_on_termination"].(bool)),
|
|
}
|
|
|
|
if v, ok := bd["volume_size"].(int); ok && v != 0 {
|
|
ebs.VolumeSize = aws.Int64(int64(v))
|
|
}
|
|
|
|
if v, ok := bd["volume_type"].(string); ok && v != "" {
|
|
ebs.VolumeType = aws.String(v)
|
|
}
|
|
|
|
if v, ok := bd["iops"].(int); ok && v > 0 && *ebs.VolumeType == "io1" {
|
|
// Only set the iops attribute if the volume type is io1. Setting otherwise
|
|
// can trigger a refresh/plan loop based on the computed value that is given
|
|
// from AWS, and prevent us from specifying 0 as a valid iops.
|
|
// See https://github.com/hashicorp/terraform/pull/4146
|
|
// See https://github.com/hashicorp/terraform/issues/7765
|
|
ebs.Iops = aws.Int64(int64(v))
|
|
} else if v, ok := bd["iops"].(int); ok && v > 0 && *ebs.VolumeType != "io1" {
|
|
// Message user about incompatibility
|
|
log.Print("[WARN] IOPs is only valid for storate type io1 for EBS Volumes")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func readVolumeTags(conn *ec2.EC2, d *schema.ResourceData) error {
|
|
volumeIds, err := getAwsInstanceVolumeIds(conn, d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tagsResp, err := conn.DescribeTags(&ec2.DescribeTagsInput{
|
|
Filters: []*ec2.Filter{
|
|
{
|
|
Name: aws.String("resource-id"),
|
|
Values: volumeIds,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var tags []*ec2.Tag
|
|
|
|
for _, t := range tagsResp.Tags {
|
|
tag := &ec2.Tag{
|
|
Key: t.Key,
|
|
Value: t.Value,
|
|
}
|
|
tags = append(tags, tag)
|
|
}
|
|
|
|
d.Set("volume_tags", tagsToMap(tags))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Determine whether we're referring to security groups with
|
|
// IDs or names. We use a heuristic to figure this out. By default,
|
|
// we use IDs if we're in a VPC. However, if we previously had an
|
|
// all-name list of security groups, we use names. Or, if we had any
|
|
// IDs, we use IDs.
|
|
func readSecurityGroups(d *schema.ResourceData, instance *ec2.Instance) error {
|
|
useID := instance.SubnetId != nil && *instance.SubnetId != ""
|
|
if v := d.Get("security_groups"); v != nil {
|
|
match := useID
|
|
sgs := v.(*schema.Set).List()
|
|
if len(sgs) > 0 {
|
|
match = false
|
|
for _, v := range v.(*schema.Set).List() {
|
|
if strings.HasPrefix(v.(string), "sg-") {
|
|
match = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
useID = match
|
|
}
|
|
|
|
// Build up the security groups
|
|
sgs := make([]string, 0, len(instance.SecurityGroups))
|
|
if useID {
|
|
for _, sg := range instance.SecurityGroups {
|
|
sgs = append(sgs, *sg.GroupId)
|
|
}
|
|
log.Printf("[DEBUG] Setting Security Group IDs: %#v", sgs)
|
|
if err := d.Set("vpc_security_group_ids", sgs); err != nil {
|
|
return err
|
|
}
|
|
if err := d.Set("security_groups", []string{}); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
for _, sg := range instance.SecurityGroups {
|
|
sgs = append(sgs, *sg.GroupName)
|
|
}
|
|
log.Printf("[DEBUG] Setting Security Group Names: %#v", sgs)
|
|
if err := d.Set("security_groups", sgs); err != nil {
|
|
return err
|
|
}
|
|
if err := d.Set("vpc_security_group_ids", []string{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type awsInstanceOpts struct {
|
|
BlockDeviceMappings []*ec2.BlockDeviceMapping
|
|
DisableAPITermination *bool
|
|
EBSOptimized *bool
|
|
Monitoring *ec2.RunInstancesMonitoringEnabled
|
|
IAMInstanceProfile *ec2.IamInstanceProfileSpecification
|
|
ImageID *string
|
|
InstanceInitiatedShutdownBehavior *string
|
|
InstanceType *string
|
|
Ipv6AddressCount *int64
|
|
Ipv6Addresses []*ec2.InstanceIpv6Address
|
|
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.Bool(d.Get("disable_api_termination").(bool)),
|
|
EBSOptimized: aws.Bool(d.Get("ebs_optimized").(bool)),
|
|
ImageID: aws.String(d.Get("ami").(string)),
|
|
InstanceType: aws.String(d.Get("instance_type").(string)),
|
|
}
|
|
|
|
if v := d.Get("instance_initiated_shutdown_behavior").(string); v != "" {
|
|
opts.InstanceInitiatedShutdownBehavior = aws.String(v)
|
|
}
|
|
|
|
opts.Monitoring = &ec2.RunInstancesMonitoringEnabled{
|
|
Enabled: aws.Bool(d.Get("monitoring").(bool)),
|
|
}
|
|
|
|
opts.IAMInstanceProfile = &ec2.IamInstanceProfileSpecification{
|
|
Name: aws.String(d.Get("iam_instance_profile").(string)),
|
|
}
|
|
|
|
user_data := d.Get("user_data").(string)
|
|
|
|
opts.UserData64 = aws.String(base64Encode([]byte(user_data)))
|
|
|
|
// 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.Print("[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))
|
|
}
|
|
}
|
|
|
|
networkInterfaces, interfacesOk := d.GetOk("network_interface")
|
|
|
|
// If setting subnet and public address, OR manual network interfaces, populate those now.
|
|
if hasSubnet && associatePublicIPAddress || interfacesOk {
|
|
// Otherwise we're attaching (a) network interface(s)
|
|
opts.NetworkInterfaces = buildNetworkInterfaceOpts(d, groups, networkInterfaces)
|
|
} else {
|
|
// If simply specifying a subnetID, privateIP, Security Groups, or VPC Security Groups, build these now
|
|
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, ok := d.GetOk("ipv6_address_count"); ok {
|
|
opts.Ipv6AddressCount = aws.Int64(int64(v.(int)))
|
|
}
|
|
|
|
if v, ok := d.GetOk("ipv6_addresses"); ok {
|
|
ipv6Addresses := make([]*ec2.InstanceIpv6Address, len(v.([]interface{})))
|
|
for _, address := range v.([]interface{}) {
|
|
ipv6Address := &ec2.InstanceIpv6Address{
|
|
Ipv6Address: aws.String(address.(string)),
|
|
}
|
|
|
|
ipv6Addresses = append(ipv6Addresses, ipv6Address)
|
|
}
|
|
|
|
opts.Ipv6Addresses = ipv6Addresses
|
|
}
|
|
|
|
if v := d.Get("vpc_security_group_ids").(*schema.Set); v.Len() > 0 {
|
|
for _, v := range v.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, d *schema.ResourceData) 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: []string{"terminated"},
|
|
Refresh: InstanceStateRefreshFunc(conn, id, ""),
|
|
Timeout: d.Timeout(schema.TimeoutDelete),
|
|
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
|
|
}
|
|
|
|
func iamInstanceProfileArnToName(ip *ec2.IamInstanceProfile) string {
|
|
if ip == nil || ip.Arn == nil {
|
|
return ""
|
|
}
|
|
parts := strings.Split(*ip.Arn, "/")
|
|
return parts[len(parts)-1]
|
|
}
|
|
|
|
func userDataHashSum(user_data string) string {
|
|
// Check whether the user_data is not Base64 encoded.
|
|
// Always calculate hash of base64 decoded value since we
|
|
// check against double-encoding when setting it
|
|
v, base64DecodeError := base64.StdEncoding.DecodeString(user_data)
|
|
if base64DecodeError != nil {
|
|
v = []byte(user_data)
|
|
}
|
|
|
|
hash := sha1.Sum(v)
|
|
return hex.EncodeToString(hash[:])
|
|
}
|
|
|
|
func getAwsInstanceVolumeIds(conn *ec2.EC2, d *schema.ResourceData) ([]*string, error) {
|
|
volumeIds := make([]*string, 0)
|
|
|
|
opts := &ec2.DescribeVolumesInput{
|
|
Filters: []*ec2.Filter{
|
|
{
|
|
Name: aws.String("attachment.instance-id"),
|
|
Values: []*string{aws.String(d.Id())},
|
|
},
|
|
},
|
|
}
|
|
|
|
resp, err := conn.DescribeVolumes(opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, v := range resp.Volumes {
|
|
volumeIds = append(volumeIds, v.VolumeId)
|
|
}
|
|
|
|
return volumeIds, nil
|
|
}
|