From 10ddf607e3d14f645a68b645e98a6e6be27b9d04 Mon Sep 17 00:00:00 2001 From: Jake Champlin Date: Mon, 24 Apr 2017 18:06:28 -0400 Subject: [PATCH] provider/aws: Add `network_interface` to instance --- .../providers/aws/resource_aws_instance.go | 225 ++++++++++++------ .../aws/resource_aws_instance_test.go | 129 +++++++++- .../providers/aws/r/instance.html.markdown | 55 +++++ 3 files changed, 337 insertions(+), 72 deletions(-) diff --git a/builtin/providers/aws/resource_aws_instance.go b/builtin/providers/aws/resource_aws_instance.go index e5d4a128f..481aa4cd1 100644 --- a/builtin/providers/aws/resource_aws_instance.go +++ b/builtin/providers/aws/resource_aws_instance.go @@ -128,18 +128,43 @@ func resourceAwsInstance() *schema.Resource { Computed: true, }, - // TODO: Deprecate me + // 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, }, - "primary_network_interface": { - ConflictsWith: []string{"associate_public_ip_address", "subnet_id", "private_ip", "vpc_security_group_ids", "security_groups", "ipv6_addresses", "ipv6_address_count"}, - Type: schema.TypeString, + "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, - ForceNew: 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": { @@ -537,25 +562,62 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { 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 { - for _, ni := range instance.NetworkInterfaces { - if *ni.Attachment.DeviceIndex == 0 { - d.Set("subnet_id", ni.SubnetId) - d.Set("network_interface_id", ni.NetworkInterfaceId) // TODO: Deprecate me - d.Set("primary_network_interface", ni.NetworkInterfaceId) - d.Set("associate_public_ip_address", ni.Association != nil) - d.Set("ipv6_address_count", len(ni.Ipv6Addresses)) - - for _, address := range ni.Ipv6Addresses { - ipv6Addresses = append(ipv6Addresses, *address.Ipv6Address) + 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 + 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 { d.Set("subnet_id", instance.SubnetId) - d.Set("network_interface_id", "") // TODO: Deprecate me - d.Set("primary_network_interface", "") + 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 { @@ -682,24 +744,28 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error { } } - 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 { - // Toloerate InvalidParameterCombination error in Classic, otherwise - // return the error - if "InvalidParameterCombination" != ec2err.Code() { - 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()) } - 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 } +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( d *schema.ResourceData, conn *ec2.EC2) ([]*ec2.BlockDeviceMapping, error) { blockDevices := make([]*ec2.BlockDeviceMapping, 0) @@ -1271,43 +1386,10 @@ func buildAwsInstanceOpts( } } - // Check if using non-defaullt primary network interface - interface_id, interfaceOk := d.GetOk("primary_network_interface") + networkInterfaces, interfacesOk := d.GetOk("network_interface") - 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(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 simply specifying a subnetID, privateIP, Security Groups, or VPC Security Groups, build these now + if !hasSubnet && !associatePublicIPAddress && !interfacesOk { if subnetID != "" { opts.SubnetID = aws.String(subnetID) } @@ -1327,6 +1409,9 @@ func buildAwsInstanceOpts( 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 { diff --git a/builtin/providers/aws/resource_aws_instance_test.go b/builtin/providers/aws/resource_aws_instance_test.go index 1de8ac768..0ba5f8024 100644 --- a/builtin/providers/aws/resource_aws_instance_test.go +++ b/builtin/providers/aws/resource_aws_instance_test.go @@ -891,7 +891,38 @@ func TestAccAWSInstance_primaryNetworkInterface(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckInstanceExists("aws_instance.foo", &instance), 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" { ami = "ami-22b9a343" 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 + } } ` diff --git a/website/source/docs/providers/aws/r/instance.html.markdown b/website/source/docs/providers/aws/r/instance.html.markdown index 4d8725de2..2edb0bd0f 100644 --- a/website/source/docs/providers/aws/r/instance.html.markdown +++ b/website/source/docs/providers/aws/r/instance.html.markdown @@ -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. * `ephemeral_block_device` - (Optional) Customize Ephemeral (also known as "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 @@ -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 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 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 * `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. +* `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 used inside the Amazon EC2, and only available if you've enabled DNS hostnames for your VPC