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,
},
// 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 {

View File

@ -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
}
}
`

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.
* `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