diff --git a/README.md b/README.md index 8c1a017..076dfe7 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The following resources will be created: - Internet Gateway - Route tables for the Public, Private, Secure and Transit subnets - Associate all Route Tables created to the correct subnet - - Nat Gateway + - Option to create Nat Gateway or Nat instance - Network Access Control List (NACL) for all subnets - Database Subnet group - Provides an RDS DB subnet group resources - S3 VPC endpoint @@ -51,17 +51,22 @@ module "network" { | Name | Version | |------|---------| | aws | n/a | +| template | n/a | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | cf\_export\_name | Name prefix for the export resources of the cloud formation output | `string` | `""` | no | +| instance\_types | Candidates of spot instance type for the NAT instance. This is used in the mixed instances policy | `list` |
[| no | | kubernetes\_clusters | List of kubernetes cluster names to creates tags in public and private subnets of this VPC | `list(string)` | `[]` | no | | kubernetes\_clusters\_type | Use either 'owned' or 'shared' for kubernetes cluster tags | `string` | `"shared"` | no | | max\_az | Max number of AZs | `number` | `3` | no | -| multi\_nat | Number of NAT Instances, 'true' will yield one per AZ while 'false' creates one NAT | `bool` | `false` | no | +| multi\_nat | Number of NAT, 'true' will yield one per AZ while 'false' creates one NAT | `bool` | `false` | no | | name | Name prefix for the resources of this stack | `any` | n/a | yes | +| nat\_architecture | Architecture type of instance | `list` |
"t4g.micro",
"t4g.small",
"t4g.medium"
]
[| no | +| nat\_gw | Create a NAT Gateway (Require: nat\_instance=false) | `bool` | `true` | no | +| nat\_instance | Create a NAT Gateway (Require: nat\_gw=false ) | `bool` | `false` | no | | newbits | Number of bits to add to the vpc cidr when building subnets | `number` | `5` | no | | private\_netnum\_offset | Start with this subnet for private ones, plus number of AZs | `number` | `5` | no | | public\_nacl\_inbound\_tcp\_ports | TCP Ports to allow inbound on public subnet via NACLs (this list cannot be empty) | `list(string)` |
"arm64"
]
[| no | @@ -83,6 +88,7 @@ module "network" { | Name | Description | |------|-------------| +| aws\_availability\_zones | aws\_availability\_zones | | cidr\_block | CIDR for VPC created | | db\_subnet\_group\_id | n/a | | internet\_gateway\_id | ID of Internet Gateway created | diff --git a/_data.tf b/_data.tf index 3f514db..7cd286f 100644 --- a/_data.tf +++ b/_data.tf @@ -1,4 +1,11 @@ -data "aws_availability_zones" "available" {} +data "aws_availability_zones" "available" { + state = "available" +} + +output "aws_availability_zones" { + value = data.aws_availability_zones.available.names + description = "aws_availability_zones" +} data "aws_availability_zone" "az" { count = length(data.aws_availability_zones.available.names) @@ -6,3 +13,29 @@ data "aws_availability_zone" "az" { } data "aws_region" "current" {} + +# AMI of the latest Amazon Linux 2 +data "aws_ami" "amazon_linux" { + most_recent = true + owners = ["amazon"] + filter { + name = "architecture" + values = var.nat_architecture + } + filter { + name = "root-device-type" + values = ["ebs"] + } + filter { + name = "name" + values = ["amzn2-ami-hvm-*"] + } + filter { + name = "virtualization-type" + values = ["hvm"] + } + filter { + name = "block-device-mapping.volume-type" + values = ["gp2"] + } +} diff --git a/_variables.tf b/_variables.tf index 514f9ff..b578dc1 100644 --- a/_variables.tf +++ b/_variables.tf @@ -23,7 +23,28 @@ variable "vpc_cidr_transit" { variable "multi_nat" { default = false - description = "Number of NAT Instances, 'true' will yield one per AZ while 'false' creates one NAT" + description = "Number of NAT, 'true' will yield one per AZ while 'false' creates one NAT" +} + +variable "nat_gw" { + default = true + description = "Create a NAT Gateway (Require: nat_instance=false)" +} + +variable "nat_instance" { + default = false + description = "Create a NAT Gateway (Require: nat_gw=false )" +} + +variable "nat_architecture" { + default = ["arm64"] + description = "Architecture type of instance" +} + +variable "instance_types" { + description = "Candidates of spot instance type for the NAT instance. This is used in the mixed instances policy" + type = list(any) + default = ["t4g.micro", "t4g.small", "t4g.medium"] } variable "newbits" { @@ -114,7 +135,6 @@ variable "kubernetes_clusters_type" { description = "Use either 'owned' or 'shared' for kubernetes cluster tags" } - locals { kubernetes_clusters = zipmap( formatlist("kubernetes.io/cluster/%s", var.kubernetes_clusters), diff --git a/cf-exports.tf b/cf-exports.tf index 47b0eec..f67951b 100644 --- a/cf-exports.tf +++ b/cf-exports.tf @@ -12,8 +12,8 @@ resource "aws_cloudformation_stack" "tf_exports" { "PrivateSubnetCidrs" = join(",", aws_subnet.private.*.cidr_block), "SecureSubnetIds" = join(",", aws_subnet.secure.*.id), "SecureSubnetCidrs" = join(",", aws_subnet.secure.*.cidr_block), - "NatGatewayIds" = join(",", aws_nat_gateway.nat_gw.*.id), - "DbSubnetGroupId" = aws_db_subnet_group.secure.id + #"NatGatewayIds" = join(",", aws_nat_gateway.nat_gw.*.id), + "DbSubnetGroupId" = aws_db_subnet_group.secure.id } }) } diff --git a/data/init.sh b/data/init.sh new file mode 100644 index 0000000..2fd2010 --- /dev/null +++ b/data/init.sh @@ -0,0 +1,95 @@ +#! /bin/bash +set -x + +echo "### INSTALL PACKAGES" +yum update -y +yum install -y amazon-efs-utils aws-cli + +echo "### INSTALL SSM AGENT" +cd /tmp +yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm + +echo "### CONFIG NETWORK INTERFACE" +echo "### Determine the region" +export AWS_DEFAULT_REGION="$(/opt/aws/bin/ec2-metadata -z | sed 's/placement: \(.*\).$/\1/')" + +echo "### Determine the instance id" +instance_id="$(/opt/aws/bin/ec2-metadata -i | cut -d' ' -f2)" + +echo "### Disable source dest check" +aws ec2 modify-instance-attribute --instance-id "$instance_id" --source-dest-check "{\"Value\": false}" + +echo "### Determine the count of EIP id" +eip_id="$(aws ec2 describe-addresses --query Addresses[*].AllocationId --filters "Name=tag:Function,Values=NAT-instance" --output text)" + +if [ $(echo "$eip_id" |wc -w) -eq 1 ]; then + echo "### Attach the EIP" + aws ec2 associate-address --instance-id "$instance_id" --allocation-id "$eip_id" + + echo "### Change the private route tables" + route_tables="$(aws ec2 describe-route-tables --query RouteTables[*].RouteTableId --filters "Name=tag:Scheme,Values=private" --output text)" + for route_id in $(echo "$route_tables") + do + route_internet="$(aws ec2 describe-route-tables --route-table-ids "$route_id" |grep -c "0.0.0.0/0")" + if [ "$route_internet" -eq 0 ] + then + aws ec2 create-route --route-table-id "$route_id" --destination-cidr-block 0.0.0.0/0 --instance-id "$instance_id" + else + aws ec2 replace-route --route-table-id "$route_id" --destination-cidr-block 0.0.0.0/0 --instance-id "$instance_id" + fi + done + + echo "### enable IP forwarding and NAT" + sysctl -q -w net.ipv4.ip_forward=1 + sysctl -q -w net.ipv4.conf.eth0.send_redirects=0 + iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE + + echo "### wait for network connection" + curl --retry 20 http://www.google.com + + echo "### reestablish connections" + systemctl restart amazon-ssm-agent + +else + echo "### Determine the network id in the zone" + timeout=600 + time_count=0 + zone_name="$(/opt/aws/bin/ec2-metadata -z | cut -d' ' -f2)" + eni_id=$(aws ec2 describe-network-interfaces --query NetworkInterfaces[*].NetworkInterfaceId --filters "Name=status,Values=available" "Name=tag:Function,Values=NAT-instance" "Name=availability-zone,Values=$zone_name" --output text) + + while [ -z $eni_id ]; do + let time_count++ + sleep 1 + eni_id=$(aws ec2 describe-network-interfaces --query NetworkInterfaces[*].NetworkInterfaceId --filters "Name=status,Values=available" "Name=tag:Function,Values=NAT-instance" "Name=availability-zone,Values=$zone_name" --output text) + if [ $time_count -eq $timeout ]; then + echo "No network interface available to instance" + shutdown -h now + fi + done + + echo "### Attach network interface" + aws ec2 attach-network-interface \ + --region "$(/opt/aws/bin/ec2-metadata -z | sed 's/placement: \(.*\).$/\1/')" \ + --instance-id "$(/opt/aws/bin/ec2-metadata -i | cut -d' ' -f2)" \ + --device-index 1 \ + --network-interface-id "$eni_id" + + while ! ip link show dev eth1; do + sleep 1 + done + + echo "### enable IP forwarding and NAT" + sysctl -q -w net.ipv4.ip_forward=1 + sysctl -q -w net.ipv4.conf.eth1.send_redirects=0 + iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE + + echo "### switch the default route to eth1" + ip route del default dev eth0 + + echo "### wait for network connection" + curl --retry 20 http://www.google.com + + echo "### reestablish connections" + systemctl restart amazon-ssm-agent + +fi \ No newline at end of file diff --git a/nat.tf b/nat.tf index 72c189b..943cb30 100644 --- a/nat.tf +++ b/nat.tf @@ -1,5 +1,6 @@ resource "aws_eip" "nat_eip" { - count = var.multi_nat ? length(data.aws_availability_zones.available.names) > var.max_az ? var.max_az : length(data.aws_availability_zones.available.names) : 1 + # count = var.multi_nat ? length(data.aws_availability_zones.available.names) > var.max_az ? var.max_az : length(data.aws_availability_zones.available.names) : var.nat_gw ? 1 : 0 + count = var.nat_gw ? var.multi_nat ? length(data.aws_availability_zones.available.names) > var.max_az ? var.max_az : length(data.aws_availability_zones.available.names) : 1 : 0 vpc = true tags = merge( @@ -7,12 +8,14 @@ resource "aws_eip" "nat_eip" { { "Name" = "${var.name}-EIP-${count.index}" "EnvName" = var.name + "Az" = upper(data.aws_availability_zone.az[count.index].name_suffix) }, ) } resource "aws_nat_gateway" "nat_gw" { - count = var.multi_nat ? length(data.aws_availability_zones.available.names) > var.max_az ? var.max_az : length(data.aws_availability_zones.available.names) : 1 + # count = var.multi_nat ? length(data.aws_availability_zones.available.names) > var.max_az ? var.max_az : length(data.aws_availability_zones.available.names) : var.nat_gw ? 1 : 0 + count = var.nat_gw ? var.multi_nat ? length(data.aws_availability_zones.available.names) > var.max_az ? var.max_az : length(data.aws_availability_zones.available.names) : 1 : 0 allocation_id = aws_eip.nat_eip[count.index].id subnet_id = aws_subnet.public[count.index].id @@ -21,6 +24,7 @@ resource "aws_nat_gateway" "nat_gw" { { "Name" = "${var.name}-NATGW-${count.index}" "EnvName" = var.name + "Az" = upper(data.aws_availability_zone.az[count.index].name_suffix) }, ) } diff --git a/nat_instance.tf b/nat_instance.tf new file mode 100644 index 0000000..0001c28 --- /dev/null +++ b/nat_instance.tf @@ -0,0 +1,198 @@ +resource "aws_network_interface" "nat_instance" { + count = var.nat_instance ? var.multi_nat ? length(data.aws_availability_zones.available.names) > var.max_az ? var.max_az : length(data.aws_availability_zones.available.names) : 1 : 0 + + security_groups = [aws_security_group.nat_instance[0].id] + subnet_id = aws_subnet.public[count.index].id + source_dest_check = false + description = "ENI for NAT instance ${count.index}" + tags = { + Name = "nat-instance-${count.index}" + Function = "NAT-instance" + } +} + +resource "aws_eip" "nat_instance" { + count = var.nat_instance ? var.multi_nat ? length(data.aws_availability_zones.available.names) > var.max_az ? var.max_az : length(data.aws_availability_zones.available.names) : 1 : 0 + network_interface = var.multi_nat ? aws_network_interface.nat_instance[count.index].id : null + tags = { + Name = "EIPforNAT_Instance-${count.index}" + Function = "NAT-instance" + } +} + +resource "aws_route" "nat_instance" { + count = var.nat_instance ? length(aws_route_table.private[*].id) : 0 + route_table_id = aws_route_table.private[count.index].id + destination_cidr_block = "0.0.0.0/0" + network_interface_id = aws_network_interface.nat_instance[count.index].id + + lifecycle { + ignore_changes = [network_interface_id] + } +} + +data "template_file" "userdata" { + template = file("${path.module}/data/init.sh") +} + +resource "aws_launch_template" "template_linux" { + count = var.nat_instance ? 1 : 0 + name = "lt-nat_instance" + image_id = data.aws_ami.amazon_linux.id + + iam_instance_profile { + arn = aws_iam_instance_profile.nat_instance[0].arn + } + + network_interfaces { + associate_public_ip_address = true + security_groups = [aws_security_group.nat_instance[0].id] + delete_on_termination = true + } + + user_data = base64encode(data.template_file.userdata.rendered) + + description = "Launch template for NAT instance ${var.name}" + tags = { + Name = "nat-instance-${var.name}" + } +} + +resource "aws_autoscaling_group" "nat_instance" { + # count = var.nat_instance ? var.multi_nat ? length(data.aws_availability_zones.available.names) : 1 : var.max_az : 1 : 0 + count = var.nat_instance ? var.multi_nat ? length(data.aws_availability_zones.available.names) > var.max_az ? var.max_az : length(data.aws_availability_zones.available.names) : 1 : 0 + name = "nat_instance-${count.index}" + capacity_rebalance = true + desired_capacity = 1 + min_size = 1 + max_size = 1 + vpc_zone_identifier = var.multi_nat ? [aws_subnet.public[count.index].id] : aws_subnet.public[*].id + + + mixed_instances_policy { + instances_distribution { + on_demand_base_capacity = 0 + on_demand_percentage_above_base_capacity = 0 + spot_allocation_strategy = var.multi_nat ? "lowest-price" : "capacity-optimized" + spot_instance_pools = var.multi_nat ? 10 : 0 + } + launch_template { + launch_template_specification { + launch_template_id = aws_launch_template.template_linux[0].id + version = "$Latest" + } + dynamic "override" { + for_each = var.instance_types + content { + instance_type = override.value + } + } + } + } + + tag { + key = "Name" + value = "nat-instance-${var.name}" + propagate_at_launch = true + } + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_iam_instance_profile" "nat_instance" { + count = var.nat_instance ? 1 : 0 + name = "${var.name}-nat_instance" + role = aws_iam_role.nat_instance[0].name +} + +resource "aws_iam_role" "nat_instance" { + count = var.nat_instance ? 1 : 0 + name = "${var.name}-nat_instance" + assume_role_policy = <
"80",
"443",
"22",
"1194"
]