provider/aws: Add `network_interface` to instance

This commit is contained in:
Jake Champlin 2017-04-24 18:06:28 -04:00
parent fe8029e65e
commit 10ddf607e3
No known key found for this signature in database
GPG Key ID: DC31F41958EF4AC2
3 changed files with 337 additions and 72 deletions

View File

@ -128,18 +128,43 @@ func resourceAwsInstance() *schema.Resource {
Computed: true, Computed: true,
}, },
// TODO: Deprecate me // TODO: Deprecate me v0.10.0
"network_interface_id": { "network_interface_id": {
Type: schema.TypeString,
Computed: true,
Deprecated: "Please use `primary_network_interface_id` instead",
},
"primary_network_interface_id": {
Type: schema.TypeString, Type: schema.TypeString,
Computed: true, Computed: true,
}, },
"primary_network_interface": { "network_interface": {
ConflictsWith: []string{"associate_public_ip_address", "subnet_id", "private_ip", "vpc_security_group_ids", "security_groups", "ipv6_addresses", "ipv6_address_count"}, 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.TypeString, Type: schema.TypeSet,
Optional: true, Optional: true,
ForceNew: true,
Computed: 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": { "public_ip": {
@ -537,25 +562,62 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("private_ip", instance.PrivateIpAddress) d.Set("private_ip", instance.PrivateIpAddress)
d.Set("iam_instance_profile", iamInstanceProfileArnToName(instance.IamInstanceProfile)) 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 var ipv6Addresses []string
if len(instance.NetworkInterfaces) > 0 { if len(instance.NetworkInterfaces) > 0 {
for _, ni := range instance.NetworkInterfaces { var primaryNetworkInterface ec2.InstanceNetworkInterface
if *ni.Attachment.DeviceIndex == 0 { var networkInterfaces []map[string]interface{}
d.Set("subnet_id", ni.SubnetId) for _, iNi := range instance.NetworkInterfaces {
d.Set("network_interface_id", ni.NetworkInterfaceId) // TODO: Deprecate me ni := make(map[string]interface{})
d.Set("primary_network_interface", ni.NetworkInterfaceId) if *iNi.Attachment.DeviceIndex == 0 {
d.Set("associate_public_ip_address", ni.Association != nil) primaryNetworkInterface = *iNi
d.Set("ipv6_address_count", len(ni.Ipv6Addresses)) }
// If the attached network device is inside our configuration, refresh state with values found.
for _, address := range ni.Ipv6Addresses { // Otherwise, assume the network device was attached via an outside resource.
ipv6Addresses = append(ipv6Addresses, *address.Ipv6Address) 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
d.Set("subnet_id", primaryNetworkInterface.SubnetId)
d.Set("network_interface_id", primaryNetworkInterface.NetworkInterfaceId) // TODO: Deprecate me v0.10.0
d.Set("primary_network_interface_id", primaryNetworkInterface.NetworkInterfaceId)
d.Set("associate_public_ip_address", primaryNetworkInterface.Association != nil)
d.Set("ipv6_address_count", len(primaryNetworkInterface.Ipv6Addresses))
for _, address := range primaryNetworkInterface.Ipv6Addresses {
ipv6Addresses = append(ipv6Addresses, *address.Ipv6Address)
}
} else { } else {
d.Set("subnet_id", instance.SubnetId) d.Set("subnet_id", instance.SubnetId)
d.Set("network_interface_id", "") // TODO: Deprecate me d.Set("network_interface_id", "") // TODO: Deprecate me v0.10.0
d.Set("primary_network_interface", "") d.Set("primary_network_interface_id", "")
} }
if err := d.Set("ipv6_addresses", ipv6Addresses); err != nil { if err := d.Set("ipv6_addresses", ipv6Addresses); err != nil {
@ -682,24 +744,28 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
} }
} }
if d.HasChange("source_dest_check") || d.IsNewResource() { // SourceDestCheck can only be modified on an instance without manually specified network interfaces.
// SourceDestCheck can only be set on VPC instances // AWS will return an error of InvalidParameterCombination if we attempt // SourceDestCheck, in that case, is configured at the network interface level
// to modify the source_dest_check of an instance in EC2 Classic if _, ok := d.GetOk("network_interface"); !ok {
log.Printf("[INFO] Modifying `source_dest_check` on Instance %s", d.Id()) if d.HasChange("source_dest_check") || d.IsNewResource() {
_, err := conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeInput{ // SourceDestCheck can only be set on VPC instances // AWS will return an error of InvalidParameterCombination if we attempt
InstanceId: aws.String(d.Id()), // to modify the source_dest_check of an instance in EC2 Classic
SourceDestCheck: &ec2.AttributeBooleanValue{ log.Printf("[INFO] Modifying `source_dest_check` on Instance %s", d.Id())
Value: aws.Bool(d.Get("source_dest_check").(bool)), _, err := conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeInput{
}, InstanceId: aws.String(d.Id()),
}) SourceDestCheck: &ec2.AttributeBooleanValue{
if err != nil { Value: aws.Bool(d.Get("source_dest_check").(bool)),
if ec2err, ok := err.(awserr.Error); ok { },
// Toloerate InvalidParameterCombination error in Classic, otherwise })
// return the error if err != nil {
if "InvalidParameterCombination" != ec2err.Code() { if ec2err, ok := err.(awserr.Error); ok {
return err // 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())
} }
log.Printf("[WARN] Attempted to modify SourceDestCheck on non VPC instance: %s", ec2err.Message())
} }
} }
} }
@ -1019,6 +1085,55 @@ func fetchRootDeviceName(ami string, conn *ec2.EC2) (*string, error) {
return rootDeviceName, nil 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 := 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( func readBlockDeviceMappingsFromConfig(
d *schema.ResourceData, conn *ec2.EC2) ([]*ec2.BlockDeviceMapping, error) { d *schema.ResourceData, conn *ec2.EC2) ([]*ec2.BlockDeviceMapping, error) {
blockDevices := make([]*ec2.BlockDeviceMapping, 0) blockDevices := make([]*ec2.BlockDeviceMapping, 0)
@ -1271,43 +1386,10 @@ func buildAwsInstanceOpts(
} }
} }
// Check if using non-defaullt primary network interface networkInterfaces, interfacesOk := d.GetOk("network_interface")
interface_id, interfaceOk := d.GetOk("primary_network_interface")
if hasSubnet && associatePublicIPAddress { // If simply specifying a subnetID, privateIP, Security Groups, or VPC Security Groups, build these now
// If we have a non-default VPC / Subnet specified, we can flag if !hasSubnet && !associatePublicIPAddress && !interfacesOk {
// AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided.
// You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise
// you get: Network interfaces and an instance-level subnet ID may not be specified on the same request
// You also need to attach Security Groups to the NetworkInterface instead of the instance,
// to avoid: Network interfaces and an instance-level security groups may not be specified on
// the same request
ni := &ec2.InstanceNetworkInterfaceSpecification{
AssociatePublicIpAddress: aws.Bool(associatePublicIPAddress),
DeviceIndex: aws.Int64(int64(0)),
SubnetId: aws.String(subnetID),
Groups: groups,
}
if v, ok := d.GetOk("private_ip"); ok {
ni.PrivateIpAddress = aws.String(v.(string))
}
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)))
}
}
opts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni}
} else if interfaceOk {
ni := &ec2.InstanceNetworkInterfaceSpecification{
DeviceIndex: aws.Int64(int64(0)),
NetworkInterfaceId: aws.String(interface_id.(string)),
}
opts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ni}
} else {
if subnetID != "" { if subnetID != "" {
opts.SubnetID = aws.String(subnetID) opts.SubnetID = aws.String(subnetID)
} }
@ -1327,6 +1409,9 @@ func buildAwsInstanceOpts(
opts.SecurityGroupIDs = append(opts.SecurityGroupIDs, aws.String(v.(string))) opts.SecurityGroupIDs = append(opts.SecurityGroupIDs, aws.String(v.(string)))
} }
} }
} else {
// Otherwise we're attaching (a) network interface(s)
opts.NetworkInterfaces = buildNetworkInterfaceOpts(d, groups, networkInterfaces)
} }
if v, ok := d.GetOk("key_name"); ok { if v, ok := d.GetOk("key_name"); ok {

View File

@ -891,7 +891,38 @@ func TestAccAWSInstance_primaryNetworkInterface(t *testing.T) {
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceExists("aws_instance.foo", &instance), testAccCheckInstanceExists("aws_instance.foo", &instance),
testAccCheckAWSENIExists("aws_network_interface.bar", &ini), testAccCheckAWSENIExists("aws_network_interface.bar", &ini),
resource.TestCheckResourceAttrSet("aws_instance.foo", "primary_network_interface"), resource.TestCheckResourceAttr("aws_instance.foo", "network_interface.#", "1"),
),
},
},
})
}
func TestAccAWSInstance_addSecondaryInterface(t *testing.T) {
var before ec2.Instance
var after ec2.Instance
var iniPrimary ec2.NetworkInterface
var iniSecondary ec2.NetworkInterface
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckInstanceDestroy,
Steps: []resource.TestStep{
{
Config: testAccInstanceConfigAddSecondaryNetworkInterfaceBefore,
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceExists("aws_instance.foo", &before),
testAccCheckAWSENIExists("aws_network_interface.primary", &iniPrimary),
resource.TestCheckResourceAttr("aws_instance.foo", "network_interface.#", "1"),
),
},
{
Config: testAccInstanceConfigAddSecondaryNetworkInterfaceAfter,
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceExists("aws_instance.foo", &after),
testAccCheckAWSENIExists("aws_network_interface.secondary", &iniSecondary),
resource.TestCheckResourceAttr("aws_instance.foo", "network_interface.#", "1"),
), ),
}, },
}, },
@ -1586,6 +1617,100 @@ resource "aws_network_interface" "bar" {
resource "aws_instance" "foo" { resource "aws_instance" "foo" {
ami = "ami-22b9a343" ami = "ami-22b9a343"
instance_type = "t2.micro" instance_type = "t2.micro"
primary_network_interface = "${aws_network_interface.bar.id}" network_interface {
network_interface_id = "${aws_network_interface.bar.id}"
device_index = 0
}
}
`
const testAccInstanceConfigAddSecondaryNetworkInterfaceBefore = `
resource "aws_vpc" "foo" {
cidr_block = "172.16.0.0/16"
tags {
Name = "tf-instance-test"
}
}
resource "aws_subnet" "foo" {
vpc_id = "${aws_vpc.foo.id}"
cidr_block = "172.16.10.0/24"
availability_zone = "us-west-2a"
tags {
Name = "tf-instance-test"
}
}
resource "aws_network_interface" "primary" {
subnet_id = "${aws_subnet.foo.id}"
private_ips = ["172.16.10.100"]
tags {
Name = "primary_network_interface"
}
}
resource "aws_network_interface" "secondary" {
subnet_id = "${aws_subnet.foo.id}"
private_ips = ["172.16.10.101"]
tags {
Name = "secondary_network_interface"
}
}
resource "aws_instance" "foo" {
ami = "ami-22b9a343"
instance_type = "t2.micro"
network_interface {
network_interface_id = "${aws_network_interface.primary.id}"
device_index = 0
}
}
`
const testAccInstanceConfigAddSecondaryNetworkInterfaceAfter = `
resource "aws_vpc" "foo" {
cidr_block = "172.16.0.0/16"
tags {
Name = "tf-instance-test"
}
}
resource "aws_subnet" "foo" {
vpc_id = "${aws_vpc.foo.id}"
cidr_block = "172.16.10.0/24"
availability_zone = "us-west-2a"
tags {
Name = "tf-instance-test"
}
}
resource "aws_network_interface" "primary" {
subnet_id = "${aws_subnet.foo.id}"
private_ips = ["172.16.10.100"]
tags {
Name = "primary_network_interface"
}
}
// Attach previously created network interface, observe no state diff on instance resource
resource "aws_network_interface" "secondary" {
subnet_id = "${aws_subnet.foo.id}"
private_ips = ["172.16.10.101"]
tags {
Name = "secondary_network_interface"
}
attachment {
instance = "${aws_instance.foo.id}"
device_index = 1
}
}
resource "aws_instance" "foo" {
ami = "ami-22b9a343"
instance_type = "t2.micro"
network_interface {
network_interface_id = "${aws_network_interface.primary.id}"
device_index = 0
}
} }
` `

View File

@ -86,6 +86,7 @@ instances. See [Shutdown Behavior](https://docs.aws.amazon.com/AWSEC2/latest/Use
instance. See [Block Devices](#block-devices) below for details. instance. See [Block Devices](#block-devices) below for details.
* `ephemeral_block_device` - (Optional) Customize Ephemeral (also known as * `ephemeral_block_device` - (Optional) Customize Ephemeral (also known as
"Instance Store") volumes on the instance. See [Block Devices](#block-devices) below for details. "Instance Store") volumes on the instance. See [Block Devices](#block-devices) below for details.
* `network_interface` - (Optional) Customize network interfaces to be attached at instance boot time. See [Network Interfaces](#network-interfaces) below for more details.
## Block devices ## Block devices
@ -149,6 +150,59 @@ resources cannot be automatically detected by Terraform. After making updates
to block device configuration, resource recreation can be manually triggered by to block device configuration, resource recreation can be manually triggered by
using the [`taint` command](/docs/commands/taint.html). using the [`taint` command](/docs/commands/taint.html).
## Network Interfaces
Each of the `network_interface` blocks attach a network interface to an EC2 Instance during boot time. However, because
the network interface is attached at boot-time, replacing/modifying the network interface **WILL** trigger a recreation
of the EC2 Instance. If you should need at any point to detach/modify/re-attach a network interface to the instance, use
the `aws_network_interface` or `aws_network_interface_attachment` resources instead.
The `network_interface` configuration block _does_, however, allow users to supply their own network interface to be used
as the default network interface on an EC2 Instance, attached at `eth0`.
Each `network_interface` block supports the following:
* `device_index` - (Required) The integer index of the network interface attachment. Limited by instance type.
* `network_interface_id` - (Required) The ID of the network interface to attach.
* `delete_on_termination` - (Optional) Whether or not to delete the network interface on instance termination. Defaults to `false`.
### Example
```hcl
resource "aws_vpc" "my_vpc" {
cidr_block = "172.16.0.0/16"
tags {
Name = "tf-example"
}
}
resource "aws_subnet" "my_subnet" {
vpc_id = "${aws_vpc.my_vpc.id}"
cidr_block = "172.16.10.0/24"
availability_zone = "us-west-2a"
tags {
Name = "tf-example"
}
}
resource "aws_network_interface" "foo" {
subnet_id = "${aws_subnet.my_subnet.id}"
private_ips = ["172.16.10.100"]
tags {
Name = "primary_network_interface"
}
}
resource "aws_instance" "foo" {
ami = "ami-22b9a343" // us-west-2
instance_type = "t2.micro"
network_interface {
network_interface_id = "${aws_network_interface.foo.id}"
device_index = 0
}
}
```
## Attributes Reference ## Attributes Reference
The following attributes are exported: The following attributes are exported:
@ -161,6 +215,7 @@ The following attributes are exported:
is only available if you've enabled DNS hostnames for your VPC is only available if you've enabled DNS hostnames for your VPC
* `public_ip` - The public IP address assigned to the instance, if applicable. **NOTE**: If you are using an [`aws_eip`](/docs/providers/aws/r/eip.html) with your instance, you should refer to the EIP's address directly and not use `public_ip`, as this field will change after the EIP is attached. * `public_ip` - The public IP address assigned to the instance, if applicable. **NOTE**: If you are using an [`aws_eip`](/docs/providers/aws/r/eip.html) with your instance, you should refer to the EIP's address directly and not use `public_ip`, as this field will change after the EIP is attached.
* `network_interface_id` - The ID of the network interface that was created with the instance. * `network_interface_id` - The ID of the network interface that was created with the instance.
* `primary_network_interface_id` - The ID of the instance's primary network interface.
* `private_dns` - The private DNS name assigned to the instance. Can only be * `private_dns` - The private DNS name assigned to the instance. Can only be
used inside the Amazon EC2, and only available if you've enabled DNS hostnames used inside the Amazon EC2, and only available if you've enabled DNS hostnames
for your VPC for your VPC