Skip to content
This repository was archived by the owner on Jul 3, 2023. It is now read-only.

Commit 5fa6c19

Browse files
Merge pull request #138 from ivanfetch/vpc_nat_instances
feat(vpc): Optionally use NAT instances instead of NAT gateways
2 parents 6ccfc88 + 1debbbf commit 5fa6c19

File tree

3 files changed

+137
-4
lines changed

3 files changed

+137
-4
lines changed

Readme.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,16 @@ traffic in and out of the different subnets. The Stack terraform will automatica
155155

156156
Traffic from each internal subnet to the outside world will run through the associated NAT gateway.
157157

158+
Alternatively, setting the `use_nat_instances` VPC module variable to true, will use [EC2 NAT instances][nat-instances] instead of the NAT gateway. NAT instances cost less than the NAT gateway, can be shutdown when not in use, and may be preferred in development environments. By default, NAT instances will not use [Elastic IPs][elastic-ip] to avoid a small hourly charge if the NAT instances are not running full time. To use Elastic IPs for the NAT instances, set the `use_eip_with_nat_instances` VPC module variable to true.
159+
158160
For further reading, check out these sources:
159161

160162
- [Recommended Address Space](http://serverfault.com/questions/630022/what-is-the-recommended-cidr-when-creating-vpc-on-aws)
161163
- [Practical VPC Design](https://medium.com/aws-activate-startup-blog/practical-vpc-design-8412e1a18dcc)
162164

163165
[nat-gateway]: http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-nat-gateway.html
166+
[nat-instances]: http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_NAT_Instance.html
167+
[elastic-ip]: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html
164168

165169
### Instances
166170

docs.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ Usage:
4444
| cidr | the CIDR block to provision for the VPC, if set to something other than the default, both internal_subnets and external_subnets have to be defined as well | `10.30.0.0/16` | no |
4545
| internal_subnets | a list of CIDRs for internal subnets in your VPC, must be set if the cidr variable is defined, needs to have as many elements as there are availability zones | `<list>` | no |
4646
| external_subnets | a list of CIDRs for external subnets in your VPC, must be set if the cidr variable is defined, needs to have as many elements as there are availability zones | `<list>` | no |
47+
| use_nat_instances | use NAT EC2 instances instead of the NAT gateway service | `false` | no |
48+
| use_eip_with_nat_instances | use Elastic IPs with NAT instances if `use_nat_instances` is true | `false` | no |
49+
| nat_instance_type | the EC2 instance type for NAT instances if `use_nat_instances` is true | `t2.nano` | no |
50+
| nat_instance_ssh_key_name | the name of the ssh key to use with NAT instances if `use_nat_instances` is true | "" | no |
4751
| availability_zones | a comma-separated list of availability zones, defaults to all AZ of the region, if set to something other than the defaults, both internal_subnets and external_subnets have to be defined as well | `<list>` | no |
4852
| bastion_instance_type | Instance type for the bastion | `t2.micro` | no |
4953
| ecs_cluster_name | the name of the cluster, if not specified the variable name will be used | `` | no |

vpc/main.tf

Lines changed: 129 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,41 @@ variable "name" {
2626
default = "stack"
2727
}
2828

29+
variable "use_nat_instances" {
30+
description = "If true, use EC2 NAT instances instead of the AWS NAT gateway service."
31+
default = false
32+
}
33+
34+
variable "nat_instance_type" {
35+
description = "Only if use_nat_instances is true, which EC2 instance type to use for the NAT instances."
36+
default = "t2.nano"
37+
}
38+
39+
variable "use_eip_with_nat_instances" {
40+
description = "Only if use_nat_instances is true, whether to assign Elastic IPs to the NAT instances. IF this is set to false, NAT instances use dynamically assigned IPs."
41+
default = false
42+
}
43+
44+
# This data source returns the newest Amazon NAT instance AMI
45+
data "aws_ami" "nat_ami" {
46+
most_recent = true
47+
48+
filter {
49+
name = "owner-alias"
50+
values = ["amazon"]
51+
}
52+
53+
filter {
54+
name = "name"
55+
values = ["amzn-ami-vpc-nat*"]
56+
}
57+
}
58+
59+
variable "nat_instance_ssh_key_name" {
60+
description = "Only if use_nat_instance is true, the optional SSH key-pair to assign to NAT instances."
61+
default = ""
62+
}
63+
2964
/**
3065
* VPC
3166
*/
@@ -55,15 +90,92 @@ resource "aws_internet_gateway" "main" {
5590
}
5691

5792
resource "aws_nat_gateway" "main" {
58-
count = "${length(var.internal_subnets)}"
93+
# Only create this if not using NAT instances.
94+
count = "${(1 - var.use_nat_instances) * length(var.internal_subnets)}"
5995
allocation_id = "${element(aws_eip.nat.*.id, count.index)}"
6096
subnet_id = "${element(aws_subnet.external.*.id, count.index)}"
6197
depends_on = ["aws_internet_gateway.main"]
6298
}
6399

64100
resource "aws_eip" "nat" {
65-
count = "${length(var.internal_subnets)}"
66-
vpc = true
101+
# Create these only if:
102+
# NAT instances are used and Elastic IPs are used with them,
103+
# or if the NAT gateway service is used (NAT instances are not used).
104+
count = "${signum((var.use_nat_instances * var.use_eip_with_nat_instances) + (var.use_nat_instances == 0 ? 1 : 0)) * length(var.internal_subnets)}"
105+
106+
vpc = true
107+
}
108+
109+
resource "aws_security_group" "nat_instances" {
110+
# Create this only if using NAT instances, vs. the NAT gateway service.
111+
count = "${0 + var.use_nat_instances}"
112+
name = "nat"
113+
description = "Allow traffic from clients into NAT instances"
114+
115+
ingress {
116+
from_port = 0
117+
to_port = 65535
118+
protocol = "udp"
119+
cidr_blocks = "${var.internal_subnets}"
120+
}
121+
122+
ingress {
123+
from_port = 0
124+
to_port = 65535
125+
protocol = "tcp"
126+
cidr_blocks = "${var.internal_subnets}"
127+
}
128+
129+
egress {
130+
from_port = 0
131+
to_port = 0
132+
protocol = "-1"
133+
cidr_blocks = ["0.0.0.0/0"]
134+
}
135+
136+
vpc_id = "${aws_vpc.main.id}"
137+
}
138+
139+
resource "aws_instance" "nat_instance" {
140+
# Create these only if using NAT instances, vs. the NAT gateway service.
141+
count = "${(0 + var.use_nat_instances) * length(var.internal_subnets)}"
142+
availability_zone = "${element(var.availability_zones, count.index)}"
143+
144+
tags {
145+
Name = "${var.name}-${format("internal-%03d NAT", count.index+1)}"
146+
Environment = "${var.environment}"
147+
}
148+
149+
volume_tags {
150+
Name = "${var.name}-${format("internal-%03d NAT", count.index+1)}"
151+
Environment = "${var.environment}"
152+
}
153+
154+
key_name = "${var.nat_instance_ssh_key_name}"
155+
ami = "${data.aws_ami.nat_ami.id}"
156+
instance_type = "${var.nat_instance_type}"
157+
source_dest_check = false
158+
159+
# associate_public_ip_address is not used,,
160+
# as public subnets have map_public_ip_on_launch set to true.
161+
# Also, using associate_public_ip_address causes issues with
162+
# stopped NAT instances which do not use an Elastic IP.
163+
# - For more details: https://github.com/terraform-providers/terraform-provider-aws/issues/343
164+
subnet_id = "${element(aws_subnet.external.*.id, count.index)}"
165+
166+
vpc_security_group_ids = ["${aws_security_group.nat_instances.id}"]
167+
168+
lifecycle {
169+
# Ignore changes to the NAT AMI data source.
170+
ignore_changes = ["ami"]
171+
}
172+
}
173+
174+
resource "aws_eip_association" "nat_instance_eip" {
175+
# Create these only if using NAT instances, vs. the NAT gateway service.
176+
count = "${(0 + (var.use_nat_instances * var.use_eip_with_nat_instances)) * length(var.internal_subnets)}"
177+
instance_id = "${element(aws_instance.nat_instance.*.id, count.index)}"
178+
allocation_id = "${element(aws_eip.nat.*.id, count.index)}"
67179
}
68180

69181
/**
@@ -125,12 +237,20 @@ resource "aws_route_table" "internal" {
125237
}
126238

127239
resource "aws_route" "internal" {
128-
count = "${length(compact(var.internal_subnets))}"
240+
# Create this only if using the NAT gateway service, vs. NAT instances.
241+
count = "${(1 - var.use_nat_instances) * length(compact(var.internal_subnets))}"
129242
route_table_id = "${element(aws_route_table.internal.*.id, count.index)}"
130243
destination_cidr_block = "0.0.0.0/0"
131244
nat_gateway_id = "${element(aws_nat_gateway.main.*.id, count.index)}"
132245
}
133246

247+
resource "aws_route" "internal_nat_instance" {
248+
count = "${(0 + var.use_nat_instances) * length(compact(var.internal_subnets))}"
249+
route_table_id = "${element(aws_route_table.internal.*.id, count.index)}"
250+
destination_cidr_block = "0.0.0.0/0"
251+
instance_id = "${element(aws_instance.nat_instance.*.id, count.index)}"
252+
}
253+
134254
/**
135255
* Route associations
136256
*/
@@ -156,6 +276,11 @@ output "id" {
156276
value = "${aws_vpc.main.id}"
157277
}
158278

279+
// The VPC CIDR
280+
output "cidr_block" {
281+
value = "${aws_vpc.main.cidr_block}"
282+
}
283+
159284
// A comma-separated list of subnet IDs.
160285
output "external_subnets" {
161286
value = ["${aws_subnet.external.*.id}"]

0 commit comments

Comments
 (0)