diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 2edb94b06608..c13934c56278 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -151,6 +151,7 @@ func Provider() terraform.ResourceProvider { "aws_launch_configuration": resourceAwsLaunchConfiguration(), "aws_lb_cookie_stickiness_policy": resourceAwsLBCookieStickinessPolicy(), "aws_main_route_table_association": resourceAwsMainRouteTableAssociation(), + "aws_nat_gateway": resourceAwsNatGateway(), "aws_network_acl": resourceAwsNetworkAcl(), "aws_network_interface": resourceAwsNetworkInterface(), "aws_opsworks_stack": resourceAwsOpsworksStack(), diff --git a/builtin/providers/aws/resource_aws_nat_gateway.go b/builtin/providers/aws/resource_aws_nat_gateway.go new file mode 100644 index 000000000000..d7c2449a8637 --- /dev/null +++ b/builtin/providers/aws/resource_aws_nat_gateway.go @@ -0,0 +1,179 @@ +package aws + +import ( + "fmt" + "log" + "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/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsNatGateway() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsNatGatewayCreate, + Read: resourceAwsNatGatewayRead, + Delete: resourceAwsNatGatewayDelete, + + Schema: map[string]*schema.Schema{ + "allocation_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "subnet_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "network_interface_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "private_ip": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "public_ip": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + } +} + +func resourceAwsNatGatewayCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + // Create the NAT Gateway + createOpts := &ec2.CreateNatGatewayInput{ + AllocationId: aws.String(d.Get("allocation_id").(string)), + SubnetId: aws.String(d.Get("subnet_id").(string)), + } + + log.Printf("[DEBUG] Create NAT Gateway: %s", *createOpts) + natResp, err := conn.CreateNatGateway(createOpts) + if err != nil { + return fmt.Errorf("Error creating NAT Gateway: %s", err) + } + + // Get the ID and store it + ng := natResp.NatGateway + d.SetId(*ng.NatGatewayId) + log.Printf("[INFO] NAT Gateway ID: %s", d.Id()) + + // Wait for the NAT Gateway to become available + log.Printf("[DEBUG] Waiting for NAT Gateway (%s) to become available", d.Id()) + stateConf := &resource.StateChangeConf{ + Pending: []string{"pending"}, + Target: "available", + Refresh: NGStateRefreshFunc(conn, d.Id()), + Timeout: 10 * time.Minute, + } + + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for NAT Gateway (%s) to become available: %s", d.Id(), err) + } + + // Update our attributes and return + return resourceAwsNatGatewayRead(d, meta) +} + +func resourceAwsNatGatewayRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + // Refresh the NAT Gateway state + ngRaw, _, err := NGStateRefreshFunc(conn, d.Id())() + if err != nil { + return err + } + if ngRaw == nil { + d.SetId("") + return nil + } + + // Set NAT Gateway attributes + ng := ngRaw.(*ec2.NatGateway) + address := ng.NatGatewayAddresses[0] + d.Set("network_interface_id", address.NetworkInterfaceId) + d.Set("private_ip", address.PrivateIp) + d.Set("public_ip", address.PublicIp) + + return nil +} + +func resourceAwsNatGatewayDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + deleteOpts := &ec2.DeleteNatGatewayInput{ + NatGatewayId: aws.String(d.Id()), + } + log.Printf("[INFO] Deleting NAT Gateway: %s", d.Id()) + + _, err := conn.DeleteNatGateway(deleteOpts) + if err != nil { + ec2err, ok := err.(awserr.Error) + if !ok { + return err + } + + if ec2err.Code() == "NatGatewayNotFound" { + return nil + } + + return err + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"deleting"}, + Target: "deleted", + Refresh: NGStateRefreshFunc(conn, d.Id()), + Timeout: 30 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 10 * time.Second, + } + + _, stateErr := stateConf.WaitForState() + if stateErr != nil { + return fmt.Errorf("Error waiting for NAT Gateway (%s) to delete: %s", d.Id(), err) + } + + return nil +} + +// NGStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch +// a NAT Gateway. +func NGStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + opts := &ec2.DescribeNatGatewaysInput{ + NatGatewayIds: []*string{aws.String(id)}, + } + resp, err := conn.DescribeNatGateways(opts) + if err != nil { + if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "NatGatewayNotFound" { + resp = nil + } else { + log.Printf("Error on NGStateRefresh: %s", err) + return nil, "", err + } + } + + if resp == nil { + // Sometimes AWS just has consistency issues and doesn't see + // our instance yet. Return an empty state. + return nil, "", nil + } + + ng := resp.NatGateways[0] + return ng, *ng.State, nil + } +} diff --git a/builtin/providers/aws/resource_aws_nat_gateway_test.go b/builtin/providers/aws/resource_aws_nat_gateway_test.go new file mode 100644 index 000000000000..40b6f77c29eb --- /dev/null +++ b/builtin/providers/aws/resource_aws_nat_gateway_test.go @@ -0,0 +1,154 @@ +package aws + +import ( + "fmt" + "strings" + "testing" + + "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/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSNatGateway_basic(t *testing.T) { + var natGateway ec2.NatGateway + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckNatGatewayDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccNatGatewayConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckNatGatewayExists("aws_nat_gateway.gateway", &natGateway), + ), + }, + }, + }) +} + +func testAccCheckNatGatewayDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_nat_gateway" { + continue + } + + // Try to find the resource + resp, err := conn.DescribeNatGateways(&ec2.DescribeNatGatewaysInput{ + NatGatewayIds: []*string{aws.String(rs.Primary.ID)}, + }) + if err == nil { + if len(resp.NatGateways) > 0 && strings.ToLower(*resp.NatGateways[0].State) != "deleted" { + return fmt.Errorf("still exists") + } + + return nil + } + + // Verify the error is what we want + ec2err, ok := err.(awserr.Error) + if !ok { + return err + } + if ec2err.Code() != "NatGatewayNotFound" { + return err + } + } + + return nil +} + +func testAccCheckNatGatewayExists(n string, ng *ec2.NatGateway) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + resp, err := conn.DescribeNatGateways(&ec2.DescribeNatGatewaysInput{ + NatGatewayIds: []*string{aws.String(rs.Primary.ID)}, + }) + if err != nil { + return err + } + if len(resp.NatGateways) == 0 { + return fmt.Errorf("NatGateway not found") + } + + *ng = *resp.NatGateways[0] + + return nil + } +} + +const testAccNatGatewayConfig = ` +resource "aws_vpc" "vpc" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "private" { + vpc_id = "${aws_vpc.vpc.id}" + cidr_block = "10.0.1.0/24" + map_public_ip_on_launch = false +} + +resource "aws_subnet" "public" { + vpc_id = "${aws_vpc.vpc.id}" + cidr_block = "10.0.2.0/24" + map_public_ip_on_launch = true +} + +resource "aws_internet_gateway" "gw" { + vpc_id = "${aws_vpc.vpc.id}" +} + +resource "aws_eip" "nat_gateway" { + vpc = true +} + +// Actual SUT +resource "aws_nat_gateway" "gateway" { + allocation_id = "${aws_eip.nat_gateway.id}" + subnet_id = "${aws_subnet.public.id}" + + depends_on = ["aws_internet_gateway.gw"] +} + +resource "aws_route_table" "private" { + vpc_id = "${aws_vpc.vpc.id}" + + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = "${aws_nat_gateway.gateway.id}" + } +} + +resource "aws_route_table_association" "private" { + subnet_id = "${aws_subnet.private.id}" + route_table_id = "${aws_route_table.private.id}" +} + +resource "aws_route_table" "public" { + vpc_id = "${aws_vpc.vpc.id}" + + route { + cidr_block = "0.0.0.0/0" + gateway_id = "${aws_internet_gateway.gw.id}" + } +} + +resource "aws_route_table_association" "public" { + subnet_id = "${aws_subnet.public.id}" + route_table_id = "${aws_route_table.public.id}" +} +` diff --git a/builtin/providers/aws/resource_aws_route.go b/builtin/providers/aws/resource_aws_route.go index 3d6f5d25bbf5..6832f8703304 100644 --- a/builtin/providers/aws/resource_aws_route.go +++ b/builtin/providers/aws/resource_aws_route.go @@ -13,7 +13,7 @@ import ( // How long to sleep if a limit-exceeded event happens var routeTargetValidationError = errors.New("Error: more than 1 target specified. Only 1 of gateway_id" + - "instance_id, network_interface_id, route_table_id or" + + "nat_gateway_id, instance_id, network_interface_id, route_table_id or" + "vpc_peering_connection_id is allowed.") // AWS Route resource Schema declaration @@ -42,6 +42,11 @@ func resourceAwsRoute() *schema.Resource { Optional: true, }, + "nat_gateway_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "instance_id": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -86,6 +91,7 @@ func resourceAwsRouteCreate(d *schema.ResourceData, meta interface{}) error { var setTarget string allowedTargets := []string{ "gateway_id", + "nat_gateway_id", "instance_id", "network_interface_id", "vpc_peering_connection_id", @@ -112,6 +118,12 @@ func resourceAwsRouteCreate(d *schema.ResourceData, meta interface{}) error { DestinationCidrBlock: aws.String(d.Get("destination_cidr_block").(string)), GatewayId: aws.String(d.Get("gateway_id").(string)), } + case "nat_gateway_id": + createOpts = &ec2.CreateRouteInput{ + RouteTableId: aws.String(d.Get("route_table_id").(string)), + DestinationCidrBlock: aws.String(d.Get("destination_cidr_block").(string)), + NatGatewayId: aws.String(d.Get("nat_gateway_id").(string)), + } case "instance_id": createOpts = &ec2.CreateRouteInput{ RouteTableId: aws.String(d.Get("route_table_id").(string)), @@ -160,6 +172,7 @@ func resourceAwsRouteRead(d *schema.ResourceData, meta interface{}) error { d.Set("destination_prefix_list_id", route.DestinationPrefixListId) d.Set("gateway_id", route.GatewayId) + d.Set("nat_gateway_id", route.NatGatewayId) d.Set("instance_id", route.InstanceId) d.Set("instance_owner_id", route.InstanceOwnerId) d.Set("network_interface_id", route.NetworkInterfaceId) @@ -176,6 +189,7 @@ func resourceAwsRouteUpdate(d *schema.ResourceData, meta interface{}) error { var setTarget string allowedTargets := []string{ "gateway_id", + "nat_gateway_id", "instance_id", "network_interface_id", "vpc_peering_connection_id", @@ -202,6 +216,12 @@ func resourceAwsRouteUpdate(d *schema.ResourceData, meta interface{}) error { DestinationCidrBlock: aws.String(d.Get("destination_cidr_block").(string)), GatewayId: aws.String(d.Get("gateway_id").(string)), } + case "nat_gateway_id": + replaceOpts = &ec2.ReplaceRouteInput{ + RouteTableId: aws.String(d.Get("route_table_id").(string)), + DestinationCidrBlock: aws.String(d.Get("destination_cidr_block").(string)), + NatGatewayId: aws.String(d.Get("nat_gateway_id").(string)), + } case "instance_id": replaceOpts = &ec2.ReplaceRouteInput{ RouteTableId: aws.String(d.Get("route_table_id").(string)), diff --git a/builtin/providers/aws/resource_aws_route_table.go b/builtin/providers/aws/resource_aws_route_table.go index 38e95363e547..752b771feff4 100644 --- a/builtin/providers/aws/resource_aws_route_table.go +++ b/builtin/providers/aws/resource_aws_route_table.go @@ -60,6 +60,11 @@ func resourceAwsRouteTable() *schema.Resource { Optional: true, }, + "nat_gateway_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "vpc_peering_connection_id": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -163,6 +168,9 @@ func resourceAwsRouteTableRead(d *schema.ResourceData, meta interface{}) error { if r.GatewayId != nil { m["gateway_id"] = *r.GatewayId } + if r.NatGatewayId != nil { + m["nat_gateway_id"] = *r.NatGatewayId + } if r.InstanceId != nil { m["instance_id"] = *r.InstanceId } @@ -282,6 +290,7 @@ func resourceAwsRouteTableUpdate(d *schema.ResourceData, meta interface{}) error RouteTableId: aws.String(d.Id()), DestinationCidrBlock: aws.String(m["cidr_block"].(string)), GatewayId: aws.String(m["gateway_id"].(string)), + NatGatewayId: aws.String(m["nat_gateway_id"].(string)), InstanceId: aws.String(m["instance_id"].(string)), VpcPeeringConnectionId: aws.String(m["vpc_peering_connection_id"].(string)), NetworkInterfaceId: aws.String(m["network_interface_id"].(string)), @@ -385,6 +394,12 @@ func resourceAwsRouteTableHash(v interface{}) int { buf.WriteString(fmt.Sprintf("%s-", v.(string))) } + natGatewaySet := false + if v, ok := m["nat_gateway_id"]; ok { + natGatewaySet = v.(string) != "" + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + instanceSet := false if v, ok := m["instance_id"]; ok { instanceSet = v.(string) != "" @@ -395,7 +410,7 @@ func resourceAwsRouteTableHash(v interface{}) int { buf.WriteString(fmt.Sprintf("%s-", v.(string))) } - if v, ok := m["network_interface_id"]; ok && !instanceSet { + if v, ok := m["network_interface_id"]; ok && !(instanceSet || natGatewaySet) { buf.WriteString(fmt.Sprintf("%s-", v.(string))) } diff --git a/website/source/docs/providers/aws/r/nat_gateway.html.markdown b/website/source/docs/providers/aws/r/nat_gateway.html.markdown new file mode 100644 index 000000000000..5f831c043db6 --- /dev/null +++ b/website/source/docs/providers/aws/r/nat_gateway.html.markdown @@ -0,0 +1,51 @@ +--- +layout: "aws" +page_title: "AWS: aws_nat_gateway" +sidebar_current: "docs-aws-resource-nat-gateway" +description: |- + Provides a resource to create a VPC NAT Gateway. +--- + +# aws\_nat\_gateway + +Provides a resource to create a VPC NAT Gateway. + +## Example Usage + +``` +resource "aws_nat_gateway" "gw" { + allocation_id = "${aws_eip.nat.id}" + subnet_id = "${aws_subnet.public.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `allocation_id` - (Required) The Allocation ID of the Elastic IP address for the gateway. +* `subnet_id` - (Required) The Subnet ID of the subnet in which to place the gateway. + +-> **Note:** It's recommended to denote that the NAT Gateway depends on the Internet Gateway for the VPC in which the NAT Gateway's subnet is located. For example: + + resource "aws_internet_gateway" "gw" { + vpc_id = "${aws_vpc.main.id}" + } + + resource "aws_nat_gateway" "gw" { + //other arguments + + depends_on = ["aws_internet_gateway.gw"] + } + + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the NAT Gateway. +* `allocation_id` - The Allocation ID of the Elastic IP address for the gateway. +* `subnet_id` - The Subnet ID of the subnet in which the NAT gateway is placed. +* `network_interface_id` - The ENI ID of the network interface created by the NAT gateway. +* `private_ip` - The private IP address of the NAT Gateway. +* `public_ip` - The public IP address of the NAT Gateway. diff --git a/website/source/docs/providers/aws/r/route.html.markdown b/website/source/docs/providers/aws/r/route.html.markdown index 3606555e6f26..299e526fd182 100644 --- a/website/source/docs/providers/aws/r/route.html.markdown +++ b/website/source/docs/providers/aws/r/route.html.markdown @@ -35,12 +35,14 @@ The following arguments are supported: * `destination_cidr_block` - (Required) The destination CIDR block. * `vpc_peering_connection_id` - (Optional) An ID of a VPC peering connection. * `gateway_id` - (Optional) An ID of a VPC internet gateway or a virtual private gateway. +* `nat_gateway_id` - (Optional) An ID of a VPC NAT gateway. * `instance_id` - (Optional) An ID of a NAT instance. * `network_interface_id` - (Optional) An ID of a network interface. -Each route must contain either a `gateway_id`, an `instance_id` or a `vpc_peering_connection_id` -or a `network_interface_id`. Note that the default route, mapping the VPC's CIDR block to "local", -is created implicitly and cannot be specified. +Each route must contain either a `gateway_id`, a `nat_gateway_id`, an +`instance_id` or a `vpc_peering_connection_id` or a `network_interface_id`. +Note that the default route, mapping the VPC's CIDR block to "local", is +created implicitly and cannot be specified. ## Attributes Reference @@ -53,5 +55,6 @@ will be exported as an attribute once the resource is created. * `destination_cidr_block` - The destination CIDR block. * `vpc_peering_connection_id` - An ID of a VPC peering connection. * `gateway_id` - An ID of a VPC internet gateway or a virtual private gateway. +* `nat_gateway_id` - An ID of a VPC NAT gateway. * `instance_id` - An ID of a NAT instance. * `network_interface_id` - An ID of a network interface. diff --git a/website/source/docs/providers/aws/r/route_table.html.markdown b/website/source/docs/providers/aws/r/route_table.html.markdown index e751b719335b..0b9c036c1ca1 100644 --- a/website/source/docs/providers/aws/r/route_table.html.markdown +++ b/website/source/docs/providers/aws/r/route_table.html.markdown @@ -45,13 +45,14 @@ Each route supports the following: * `cidr_block` - (Required) The CIDR block of the route. * `gateway_id` - (Optional) The Internet Gateway ID. +* `nat_gateway_id` - (Optional) The NAT Gateway ID. * `instance_id` - (Optional) The EC2 instance ID. * `vpc_peering_connection_id` - (Optional) The VPC Peering ID. * `network_interface_id` - (Optional) The ID of the elastic network interface (eni) to use. -Each route must contain either a `gateway_id`, an `instance_id` or a `vpc_peering_connection_id` -or a `network_interface_id`. Note that the default route, mapping the VPC's CIDR block to "local", -is created implicitly and cannot be specified. +Each route must contain either a `gateway_id`, an `instance_id`, a `nat_gateway_id`, a +`vpc_peering_connection_id` or a `network_interface_id`. Note that the default route, mapping +the VPC's CIDR block to "local", is created implicitly and cannot be specified. ## Attributes Reference diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index c2df5bbf5c9b..56feb497097f 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -530,6 +530,10 @@ aws_main_route_table_association + > + aws_nat_gateway + + > aws_network_acl