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

Overhaul for IPv6 and flexiblity #159

Merged
merged 12 commits into from
May 15, 2022
309 changes: 199 additions & 110 deletions README.md

Large diffs are not rendered by default.

96 changes: 85 additions & 11 deletions README.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,73 @@ related:
url: "https://github.com/cloudposse/terraform-aws-named-subnets"
# Short description of this project
description: |-
Terraform module to provision public and private [`subnets`](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html) in an existing [`VPC`](https://aws.amazon.com/vpc)
Terraform module to provision public and private [`subnets`](https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html) in an existing [`VPC`](https://aws.amazon.com/vpc)

**IMPORTANT:** This module provisions NAT instance with public IP.
__Note:__ this module is intended for use with an existing VPC and existing Internet Gateway.

__Note:__ This module is intended for use with an existing VPC and existing Internet Gateway.
To create a new VPC, use [terraform-aws-vpc](https://github.com/cloudposse/terraform-aws-vpc) module.

__Note:__ Due to Terraform [limitations](https://github.com/hashicorp/terraform/issues/26755#issuecomment-719103775),
many optional inputs to this module are specified as a `list(string)` that can have zero or one element, rather than
as a `string` that could be empty or `null`. The designation of an input as a `list` type does not necessarily
mean that you can supply more than one value in the list, so check the input's description before supplying more than one value.

The core function of this module is to create 2 sets of subnets, a "public" set with bidirectional access to the
public internet, and a "private" set behind a firewall with egress-only access to the public internet. This
includes dividing up a given CIDR range so that a each subnet gets its own
distinct CIDR range within that range, and then creating those subnets in the appropriate availability zones.
The intention is to keep this module relatively simple and easy to use for the most popular use cases.
In its default configuration, this module creates 1 public subnet and 1 private subnet in each
of the specified availability zones. The public subnets are configured for bi-directional traffic to the
public internet, while the private subnets are configured for egress-only traffic to the public internet.
Rather than provide a wealth of configuration options allowing for numerous special cases, this module
provides some common options and further provides the ability to suppress the creation of resources, allowing
you to create and configure them as you like from outside this module. For example, rather than give you the
option to customize the Network ACL, the module gives you the option to create a completely open one (and control
access via Security Groups and other means) or not create one at all, allowing you to create and configure one yourself.

### Public subnets

This module defines a public subnet as one that has direct access to an internet gateway and can accept incoming connection requests.
In the simplest configuration, the module creates a single route table with a default route targeted to the
VPC's internet gateway, and associates all the public subnets with that single route table.

Likewise it creates a single Network ACL with associated rules allowing all ingress and all egress,
and associates that ACL with all the public subnets.

### Private subnets

A private subnet may be able to initiate traffic to the public internet through a NAT gateway,
a NAT instance, or an egress-only internet gateway, or it might only have direct access to other
private subnets. In the simple configuration, for IPv4 and/or IPv6 with NAT64 enabled via `public_dns64_enabled`
or `private_dns64_enabled`, the module creates 1 NAT Gateway or NAT Instance for each
private subnet (in the public subnet in the same availability zone), creates 1 route table for each private subnet,
and adds to that route table a default route from the subnet to its NAT Gateway or Instance. For IPv6,
the module adds a route to the Egress-Only Internet Gateway configured via input.

As with the Public subnets, the module creates a single Network ACL with associated rules allowing all ingress and
all egress, and associates that ACL with all the private subnets.

### Customization for special use cases

Various features are controlled by `bool` inputs with names ending in `_enabled`. By changing the default
values, you can enable or disable creation of public subnets, private subnets, route tables,
NAT gateways, NAT instances, or Network ACLs. So for example, you could use this module to create only
private subnets and the open Network ACL, and then add your own route table associations to the subnets
and route all non-local traffic to a Transit Gateway or VPN.

### CIDR allocation

For IPv4, you provide a CIDR and the module divides the address space into the largest CIDRs possible that are still
small enough to accommodate `max_subnet_count` subnets of each enabled type (public or private). When `max_subnet_count`
is left at the default `0`, it is set to the total number of availability zones in the region. Private subnets
are allocated out of the first half of the reserved range, and public subnets are allocated out of the second half.

For IPv6, you provide a `/56` CIDR and the module assigns `/64` subnets of that CIDR in consecutive order starting
at zero. (You have the option of specifying a list of CIDRs instead.) As with IPv4, enough CIDRs are allocated to
cover `max_subnet_count` private and public subnets (when both are enabled, which is the default), with the private
subnets being allocated out of the lower half of the reservation and the public subnets allocated out of the upper half.

# How to use this project
usage: |-
```hcl
Expand All @@ -63,30 +125,40 @@ usage: |-
stage = "prod"
name = "app"
vpc_id = "vpc-XXXXXXXX"
igw_id = "igw-XXXXXXXX"
cidr_block = "10.0.0.0/16"
igw_id = ["igw-XXXXXXXX"]
ipv4_cidr_block = "10.0.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b"]
}
```

Create only private subnets, route to transit gateway:

```hcl
module "subnets_with_existing_ips" {
module "private_tgw_subnets" {
source = "cloudposse/dynamic-subnets/aws"
# Cloud Posse recommends pinning every module to a specific version
# version = "x.x.x"
namespace = "eg"
stage = "prod"
name = "app"
vpc_id = "vpc-XXXXXXXX"
igw_id = "igw-XXXXXXXX"
cidr_block = "10.0.0.0/16"
igw_id = ["igw-XXXXXXXX"]
ipv4_cidr_block = "10.0.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b"]
nat_gateway_enabled = true
nat_elastic_ips = ["1.2.3.4", "1.2.3.5"]

nat_gateway_enabled = false
public_subnets_enabled = false
}

resource "aws_route" "private" {
count = length(module.private_tgw_subnets.private_route_table_ids)

route_table_id = module.private_tgw_subnets.private_route_table_ids[count.index]
destination_cidr_block = "0.0.0.0/0"
transit_gateway_id = "tgw-XXXXXXXXX"
}
```

Learn about [using providers](https://www.terraform.io/docs/configuration-0-11/modules.html#providers-within-modules) with terraform modules.
include:
- "docs/design.md"
- "docs/targets.md"
Expand All @@ -97,6 +169,8 @@ contributors:
github: "osterman"
- name: "Andriy Knysh"
github: "aknysh"
- name: "Nuru"
github: "Nuru"
- name: "Sergey Vasilyev"
github: "s2504s"
- name: "Vladimir"
Expand Down
93 changes: 30 additions & 63 deletions docs/design.md
Original file line number Diff line number Diff line change
@@ -1,72 +1,39 @@
## Subnet calculation logic

`terraform-aws-dynamic-subnets` creates a set of subnets based on `${var.cidr_block}` input and number of Availability Zones in the region.

For subnet set calculation, the module uses Terraform interpolation

[cidrsubnet](https://www.terraform.io/docs/configuration/interpolation.html#cidrsubnet-iprange-newbits-netnum-).
`terraform-aws-dynamic-subnets` creates a set of subnets based on various CIDR inputs and
the maximum possible number of subnets, which is `max_subnet_count` when specified or
the number of Availability Zones in the region when `max_subnet_count` is left at
its default value of zero.

You can explicitly provide CIDRs for subnets via `ipv4_cidrs` and `ipv6_cidrs` inputs if you want,
but the usual use case is to provide a single CIDR which this module will subdivide into a set
of CIDRs as follows:

1. Get number of available AZ in the region:
```
${
cidrsubnet(
signum(length(var.cidr_block)) == 1 ?
var.cidr_block : data.aws_vpc.default.cidr_block,
ceil(log(length(data.aws_availability_zones.available.names) * 2, 2)),
count.index)
}
existing_az_count = length(data.aws_availability_zones.available.names)
```
2. Determine how many sets of subnets are being created. (Usually it is `2`: `public` and `private`): `subnet_type_count`.
3. Multiply the results of (1) and (2) to determine how many CIDRs to reserve:
```
cidr_count = existing_az_count * subnet_type_count
```

4. Calculate the number of bits needed to enumerate all the CIDRs:
```
subnet_bits = ceil(log(cidr_count, 2))
```
5. Reserve CIDRs for private subnets using [`cidrsubnet`](https://www.terraform.io/language/functions/cidrsubnet):
```
private_subnet_cidrs = [ for netnumber in range(0, existing_az_count): cidrsubnet(cidr_block, subnet_bits, netnumber) ]
```
6. Reserve CIDRs for public subnets in the second half of the CIDR block:
```
public_subnet_cidrs = [ for netnumber in range(existing_az_count, existing_az_count * 2): cidrsubnet(cidr_block, subnet_bits, netnumber) ]
```

1. Use `${var.cidr_block}` input (if specified) or
use a VPC CIDR block `data.aws_vpc.default.cidr_block` (e.g. `10.0.0.0/16`)
2. Get number of available AZ in the region (e.g. `length(data.aws_availability_zones.available.names)`)
3. Calculate `newbits`. `newbits` number specifies how many subnets
be the CIDR block (input or VPC) will be divided into. `newbits` is the number of `binary digits`.

Example:

`newbits = 1` - 2 subnets are available (`1 binary digit` allows to count up to `2`)

`newbits = 2` - 4 subnets are available (`2 binary digits` allows to count up to `4`)

`newbits = 3` - 8 subnets are available (`3 binary digits` allows to count up to `8`)

etc.

1. We know, that we have `6` AZs in a `us-east-1` region (see step 2).
2. We need to create `1 public` subnet and `1 private` subnet in each AZ,
thus we need to create `12 subnets` in total (`6` AZs * (`1 public` + `1 private`)).
3. We need `4 binary digits` for that ( 2<sup>4</sup> = 16 ).
In order to calculate the number of `binary digits` we should use `logarithm`
function. We should use `base 2` logarithm because decimal numbers
can be calculated as `powers` of binary number.
See [Wiki](https://en.wikipedia.org/wiki/Binary_number#Decimal)
for more details

Example:

For `12 subnets` we need `3.58` `binary digits` (log<sub>2</sub>12)

For `16 subnets` we need `4` `binary digits` (log<sub>2</sub>16)

For `7 subnets` we need `2.81` `binary digits` (log<sub>2</sub>7)

etc.
4. We can't use fractional values to calculate the number of `binary digits`.
We can't round it down because smaller number of `binary digits` is
insufficient to represent the required subnets.
We round it up. See [ceil](https://www.terraform.io/docs/configuration/interpolation.html#ceil-float-).

Example:

For `12 subnets` we need `4` `binary digits` (ceil(log<sub>2</sub>12))

For `16 subnets` we need `4` `binary digits` (ceil(log<sub>2</sub>16))

For `7 subnets` we need `3` `binary digits` (ceil(log<sub>2</sub>7))

etc.

5. Assign private subnets according to AZ number (we're using `count.index` for that).
6. Assign public subnets according to AZ number but with a shift according to the number of AZs in the region (see step 2)
Note that this means that, for example, in a region with 4 availability zones, if you specify only 3 availability zones
Copy link
Member

Choose a reason for hiding this comment

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

nit: consider eliminating the second that:

Note that this means, for example, ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Eliminating optional/implied words in the fashion you suggest works for me, but it seems to be a problem for readers for whom English is not their native language.

in `var.availability_zones`, this module will still reserve CIDRs for the 4th zone. This is so that if you later
want to expand into that zone, the existing subnet CIDR assignments will not be disturbed. If you do not want
to reserve these CIDRs, set `max_subnet_count` to the number of zones you are actually using.
Loading