Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

provider/aws: EC2 instance - multiple private ips #6387

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions builtin/providers/aws/resource_aws_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,20 @@ func resourceAwsInstance() *schema.Resource {
},

"private_ip": &schema.Schema{
Type: schema.TypeString,
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
Deprecated: "Please use the `private_ips` field instead",
},

"private_ips": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},

"source_dest_check": &schema.Schema{
Expand Down Expand Up @@ -491,6 +501,7 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
for _, ni := range instance.NetworkInterfaces {
if *ni.Attachment.DeviceIndex == 0 {
d.Set("subnet_id", ni.SubnetId)
d.Set("private_ips", flattenInstancePrivateIpAddresses(ni.PrivateIpAddresses))
}
}
} else {
Expand Down Expand Up @@ -1049,7 +1060,24 @@ func buildAwsInstanceOpts(
}
}

if hasSubnet && associatePublicIPAddress {
private_ips := d.Get("private_ips").(*schema.Set).List()
if len(private_ips) != 0 {
Copy link
Member

@radeksimko radeksimko Jul 9, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll need to refactor the whole conditional block to keep handling EC2 Classic vs VPC correctly.
I'm still ok with deprecating private_ip in favour of private_ips, but we will need to add some checks for EC2 Classic && len(private_ips) > 1 cases. I think we should error out as early as we can in such cases.

See #7568 which may help in making decisions. We need to be prepared for scenarios where we don't have the list of supported platforms though - I'd suggest we 1st try assigning multiple IPs and error out with an informative error message (e.g. ("Assigning %d IPs failed: %s (hint: EC2 Classic accounts don't support multiple private IPs for EC2 instances)", len(ips), err)).

I'm aware that it may be difficult to test on EC2 Classic and many/most of us run VPC-only accounts, but we can at least stick to expectations described in AWS docs: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-vpc.html#differences-ec2-classic-vpc

I recommend carefully reading specifically the following two sections:

  • Multiple private IP addresses
  • Accessing the Internet

ni := &ec2.InstanceNetworkInterfaceSpecification{
AssociatePublicIpAddress: aws.Bool(associatePublicIPAddress),
DeviceIndex: aws.Int64(int64(0)),
SubnetId: aws.String(subnetID),
Groups: groups,
PrivateIpAddresses: expandPrivateIPAddresses(private_ips),
}

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 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
Expand Down Expand Up @@ -1148,3 +1176,13 @@ func iamInstanceProfileArnToName(ip *ec2.IamInstanceProfile) string {
parts := strings.Split(*ip.Arn, "/")
return parts[len(parts)-1]
}

//Flattens an array of private ip addresses into a []string, where the elements returned are the IP strings e.g. "192.168.0.0"
func flattenInstancePrivateIpAddresses(dtos []*ec2.InstancePrivateIpAddress) []string {
ips := make([]string, 0, len(dtos))
for _, v := range dtos {
ip := *v.PrivateIpAddress
ips = append(ips, ip)
}
return ips
}
87 changes: 87 additions & 0 deletions builtin/providers/aws/resource_aws_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,50 @@ func TestAccAWSInstance_privateIP(t *testing.T) {
})
}

func TestAccAWSInstance_privateIPs(t *testing.T) {
var v ec2.Instance

testCheckPrivateIPs := func() resource.TestCheckFunc {
return func(*terraform.State) error {
if l := len(v.NetworkInterfaces); l != 1 {
return fmt.Errorf("expected 1 eni, got %s", l)
}

privateIps := flattenInstancePrivateIpAddresses(v.NetworkInterfaces[0].PrivateIpAddresses)
if l := len(privateIps); l != 2 {
return fmt.Errorf("expected 2 private IPs, got %s", l)
}

ip1 := privateIps[0]
ip2 := privateIps[1]
if ip1 != "10.1.1.42" && ip2 != "10.1.1.42" {
return fmt.Errorf("expected private IP 10.1.1.42 to be set")
}
if ip1 != "10.1.1.43" && ip2 != "10.1.1.43" {
return fmt.Errorf("expected private IP 10.1.1.43 to be set")
}

return nil
}
}

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "aws_instance.foo",
Providers: testAccProviders,
CheckDestroy: testAccCheckInstanceDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccInstanceConfigPrivateIPs,
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceExists("aws_instance.foo", &v),
testCheckPrivateIPs(),
),
},
},
})
}

func TestAccAWSInstance_associatePublicIPAndPrivateIP(t *testing.T) {
var v ec2.Instance

Expand Down Expand Up @@ -700,6 +744,31 @@ func driftTags(instance *ec2.Instance) resource.TestCheckFunc {
}
}

func TestFlattenInstancePrivateIpAddresses(t *testing.T) {
expanded := []*ec2.InstancePrivateIpAddress{
&ec2.InstancePrivateIpAddress{PrivateIpAddress: aws.String("192.168.0.1")},
&ec2.InstancePrivateIpAddress{PrivateIpAddress: aws.String("192.168.0.2")},
}

result := flattenInstancePrivateIpAddresses(expanded)

if result == nil {
t.Fatal("result was nil")
}

if len(result) != 2 {
t.Fatalf("expected result had %d elements, but got %d", 2, len(result))
}

if result[0] != "192.168.0.1" {
t.Fatalf("expected ip to be 192.168.0.1, but was %s", result[0])
}

if result[1] != "192.168.0.2" {
t.Fatalf("expected ip to be 192.168.0.2, but was %s", result[1])
}
}

const testAccInstanceConfig_pre = `
resource "aws_security_group" "tf_test_foo" {
name = "tf_test_foo"
Expand Down Expand Up @@ -921,6 +990,24 @@ resource "aws_instance" "foo" {
}
`

const testAccInstanceConfigPrivateIPs = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
}

resource "aws_subnet" "foo" {
cidr_block = "10.1.1.0/24"
vpc_id = "${aws_vpc.foo.id}"
}

resource "aws_instance" "foo" {
ami = "ami-c5eabbf5"
instance_type = "t2.micro"
subnet_id = "${aws_subnet.foo.id}"
private_ips = ["10.1.1.42","10.1.1.43"]
}
`

const testAccInstanceConfigAssociatePublicIPAndPrivateIP = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
Expand Down
25 changes: 14 additions & 11 deletions website/source/docs/providers/aws/r/instance.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ and deleted. Instances also support [provisioning](/docs/provisioners/index.html
## Example Usage

```
# Create a new instance of the `ami-408c7f28` (Ubuntu 14.04) on an
# Create a new instance of the `ami-408c7f28` (Ubuntu 14.04) on an
# t1.micro node with an AWS Tag naming it "HelloWorld"
provider "aws" {
region = "us-east-1"
}

resource "aws_instance" "web" {
ami = "ami-408c7f28"
instance_type = "t1.micro"
Expand All @@ -41,9 +41,9 @@ The following arguments are supported:
EBS-optimized.
* `disable_api_termination` - (Optional) If true, enables [EC2 Instance
Termination Protection](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/terminating-instances.html#Using_ChangingDisableAPITermination)
* `instance_initiated_shutdown_behavior` - (Optional) Shutdown behavior for the
instance. Amazon defaults this to `stop` for EBS-backed instances and
`terminate` for instance-store instances. Cannot be set on instance-store
* `instance_initiated_shutdown_behavior` - (Optional) Shutdown behavior for the
instance. Amazon defaults this to `stop` for EBS-backed instances and
`terminate` for instance-store instances. Cannot be set on instance-store
instances. See [Shutdown Behavior](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/terminating-instances.html#Using_ChangingInstanceInitiatedShutdownBehavior) for more information.
* `instance_type` - (Required) The type of instance to start
* `key_name` - (Optional) The key name to use for the instance.
Expand All @@ -52,9 +52,11 @@ instances. See [Shutdown Behavior](https://docs.aws.amazon.com/AWSEC2/latest/Use
If you are within a non-default VPC, you'll need to use `vpc_security_group_ids` instead.
* `vpc_security_group_ids` - (Optional) A list of security group IDs to associate with.
* `subnet_id` - (Optional) The VPC Subnet ID to launch in.
* `associate_public_ip_address` - (Optional) Associate a public ip address with an instance in a VPC. Boolean value.
* `private_ip` - (Optional) Private IP address to associate with the
instance in a VPC.
* `associate_public_ip_address` - (Optional) Associate a public ip address with an instance in a VPC. Boolean value.
* `private_ip` - (Optional, Deprecated) Private IP address to associate with the
instance in a VPC. This
attribute is deprecated, please use the `private_ips` attribute instead.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would omit any description for deprecated arguments - it should be mentioned in docs so people aren't confused why things still work, but we should make it more obvious that things just won't work in the near future.

i.e.

* `private_ip` - Deprecated, use `private_ips` instead.

* `private_ips` - (Optional) A list of private IP addresses to accociate with the instance's first network interface in a VPC.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

associate not accociate

* `source_dest_check` - (Optional) Controls if traffic is routed to the instance when
the destination address does not match the instance. Used for NAT or VPNs. Defaults true.
* `user_data` - (Optional) The user data to provide when launching the instance.
Expand Down Expand Up @@ -136,13 +138,14 @@ The following attributes are exported:
* `availability_zone` - The availability zone of the instance.
* `placement_group` - The placement group of the instance.
* `key_name` - The key name of the instance
* `public_dns` - The public DNS name assigned to the instance. For EC2-VPC, this
* `public_dns` - The public DNS name assigned to the instance. For EC2-VPC, this
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.
* `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
* `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
* `private_ip` - The private IP address assigned to the instance
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd turn this line into:

`private_ip` - Deprecated, use `private_ips[0]` instead

* `private_ips` - A list of private IP addresses assigned to the instance's first network interface.
* `security_groups` - The associated security groups.
* `vpc_security_group_ids` - The associated security groups in non-default VPC
* `subnet_id` - The VPC subnet ID.