├── modules ├── nat │ ├── README.md │ ├── output.tf │ ├── variables.tf │ └── main.tf ├── vpc │ ├── README.md │ ├── variables.tf │ ├── output.tf │ └── main.tf ├── bastion │ ├── README.md │ ├── output.tf │ ├── iam.tf │ ├── variables.tf │ ├── user-data │ │ └── user-data.sh │ └── main.tf ├── openvpn │ └── README.md ├── public-subnet │ ├── README.md │ ├── output.tf │ ├── variables.tf │ └── main.tf ├── private-app-subnet │ ├── README.md │ ├── output.tf │ ├── variables.tf │ └── main.tf ├── private-persistence-subnet │ ├── README.md │ ├── output.tf │ ├── variables.tf │ └── main.tf ├── network-acl │ ├── output.tf │ ├── variables.tf │ └── main.tf ├── spare-subnet │ ├── output.tf │ ├── main.tf │ └── variables.tf ├── vpc-peering │ ├── main.tf │ ├── variables.tf │ └── README.md └── network.tf ├── .gitignore ├── diagrams └── networking.png └── README.md /modules/nat/README.md: -------------------------------------------------------------------------------- 1 | # nat 2 | -------------------------------------------------------------------------------- /modules/vpc/README.md: -------------------------------------------------------------------------------- 1 | # vpc 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .history 2 | *.orig 3 | .idea -------------------------------------------------------------------------------- /modules/bastion/README.md: -------------------------------------------------------------------------------- 1 | # bastion 2 | -------------------------------------------------------------------------------- /modules/openvpn/README.md: -------------------------------------------------------------------------------- 1 | # openvpn 2 | -------------------------------------------------------------------------------- /modules/public-subnet/README.md: -------------------------------------------------------------------------------- 1 | # public-subnet 2 | -------------------------------------------------------------------------------- /modules/private-app-subnet/README.md: -------------------------------------------------------------------------------- 1 | # private-app-subnet 2 | -------------------------------------------------------------------------------- /modules/private-persistence-subnet/README.md: -------------------------------------------------------------------------------- 1 | # private-persistence-subnet 2 | -------------------------------------------------------------------------------- /modules/network-acl/output.tf: -------------------------------------------------------------------------------- 1 | output "network_acl_id" { 2 | value = aws_network_acl.acl.id 3 | } 4 | -------------------------------------------------------------------------------- /modules/vpc/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | default = "vpc" 3 | } 4 | 5 | variable "vpc_cidr" {} 6 | -------------------------------------------------------------------------------- /diagrams/networking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stakater/blueprint-network-aws/HEAD/diagrams/networking.png -------------------------------------------------------------------------------- /modules/spare-subnet/output.tf: -------------------------------------------------------------------------------- 1 | output "subnet_ids" { 2 | value = join(",", aws_subnet.spare.*.id) 3 | } 4 | -------------------------------------------------------------------------------- /modules/private-persistence-subnet/output.tf: -------------------------------------------------------------------------------- 1 | output "subnet_ids" { 2 | value = join(",", aws_subnet.persistence.*.id) 3 | } 4 | -------------------------------------------------------------------------------- /modules/vpc/output.tf: -------------------------------------------------------------------------------- 1 | output "vpc_id" { 2 | value = aws_vpc.vpc.id 3 | } 4 | 5 | output "vpc_cidr" { 6 | value = aws_vpc.vpc.cidr_block 7 | } 8 | -------------------------------------------------------------------------------- /modules/bastion/output.tf: -------------------------------------------------------------------------------- 1 | output "ssh_user" { 2 | value = var.ssh_user 3 | } 4 | 5 | output "security_group_id" { 6 | value = aws_security_group.bastion.id 7 | } 8 | -------------------------------------------------------------------------------- /modules/nat/output.tf: -------------------------------------------------------------------------------- 1 | output "nat_gateway_ids" { 2 | value = join(",", aws_nat_gateway.nat.*.id) 3 | } 4 | 5 | output "public_ip" { 6 | value = aws_eip.nat.public_ip 7 | } 8 | -------------------------------------------------------------------------------- /modules/network-acl/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | default = "acl" 3 | } 4 | 5 | variable "vpc_id" {} 6 | variable "public_subnet_ids" {} 7 | variable "private_app_subnet_ids" {} 8 | -------------------------------------------------------------------------------- /modules/public-subnet/output.tf: -------------------------------------------------------------------------------- 1 | output "subnet_ids" { 2 | value = join(",", aws_subnet.public.*.id) 3 | } 4 | 5 | output "route_table_ids" { 6 | value = join(",", aws_route_table.public.*.id) 7 | } 8 | -------------------------------------------------------------------------------- /modules/private-app-subnet/output.tf: -------------------------------------------------------------------------------- 1 | output "subnet_ids" { 2 | value = join(",", aws_subnet.private.*.id) 3 | } 4 | 5 | output "route_table_ids" { 6 | value = join(",", aws_route_table.private.*.id) 7 | } 8 | -------------------------------------------------------------------------------- /modules/vpc/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_vpc" "vpc" { 2 | cidr_block = var.vpc_cidr 3 | 4 | tags { 5 | Name = var.name 6 | } 7 | 8 | enable_dns_support = true 9 | enable_dns_hostnames = true 10 | } 11 | -------------------------------------------------------------------------------- /modules/nat/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | default = "nat" 3 | } 4 | 5 | variable "public_subnet_ids" {} 6 | 7 | variable "azs" { 8 | description = "A list of Availability zones in the region" 9 | default = [] 10 | } 11 | -------------------------------------------------------------------------------- /modules/spare-subnet/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_subnet" "spare" { 2 | vpc_id = var.vpc_id 3 | cidr_block = var.spare_subnets[count.index] 4 | availability_zone = var.azs[count.index] 5 | count = length(var.azs) 6 | 7 | tags { 8 | Name = "${var.name}-${var.azs[count.index]}" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /modules/public-subnet/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | default = "public" 3 | } 4 | 5 | variable "vpc_id" {} 6 | 7 | variable "public_subnets" { 8 | description = "A list of CIDR blocks for public subnets inside the VPC." 9 | default = [] 10 | } 11 | 12 | variable "azs" { 13 | description = "A list of Availability zones in the region" 14 | default = [] 15 | } 16 | -------------------------------------------------------------------------------- /modules/spare-subnet/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | default = "spare" 3 | } 4 | 5 | variable "vpc_id" {} 6 | 7 | variable "spare_subnets" { 8 | description = "A list of CIDR blocks for spare subnets inside the VPC." 9 | type = list 10 | } 11 | 12 | variable "azs" { 13 | description = "A list of Availability zones in the region" 14 | type = list 15 | } 16 | -------------------------------------------------------------------------------- /modules/private-persistence-subnet/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | default = "persistence" 3 | } 4 | 5 | variable "vpc_id" {} 6 | 7 | variable "private_persistence_subnets" { 8 | description = "A list of CIDR blocks for private persistence subnets inside the VPC." 9 | default = [] 10 | } 11 | 12 | variable "azs" { 13 | description = "A list of Availability zones in the region" 14 | default = [] 15 | } 16 | -------------------------------------------------------------------------------- /modules/private-app-subnet/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | default = "private-app" 3 | } 4 | 5 | variable "vpc_id" {} 6 | variable "nat_gateway_ids" {} 7 | 8 | variable "private_app_subnets" { 9 | description = "A list of CIDR blocks for private app subnets inside the VPC." 10 | default = [] 11 | } 12 | 13 | variable "azs" { 14 | description = "A list of Availability zones in the region" 15 | default = [] 16 | } 17 | -------------------------------------------------------------------------------- /modules/nat/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_eip" "nat" { 2 | vpc = true 3 | 4 | count = length(var.azs) # Comment out count to only have 1 NAT 5 | 6 | lifecycle { 7 | create_before_destroy = true 8 | } 9 | } 10 | 11 | resource "aws_nat_gateway" "nat" { 12 | count = length(var.azs) # Comment out count to only have 1 NAT 13 | 14 | allocation_id = element(aws_eip.nat.*.id, count.index) 15 | subnet_id = element(split(",", var.public_subnet_ids), count.index) 16 | 17 | lifecycle { 18 | create_before_destroy = true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modules/network-acl/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_network_acl" "acl" { 2 | vpc_id = var.vpc_id 3 | subnet_ids = [concat(split(",", var.public_subnet_ids), split(",", var.private_app_subnet_ids))] 4 | 5 | ingress { 6 | protocol = "-1" 7 | rule_no = 100 8 | action = "allow" 9 | cidr_block = "0.0.0.0/0" 10 | from_port = 0 11 | to_port = 0 12 | } 13 | 14 | egress { 15 | protocol = "-1" 16 | rule_no = 100 17 | action = "allow" 18 | cidr_block = "0.0.0.0/0" 19 | from_port = 0 20 | to_port = 0 21 | } 22 | 23 | tags { 24 | Name = var.name 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modules/private-persistence-subnet/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_route_table" "persistence" { 2 | vpc_id = var.vpc_id 3 | 4 | tags { 5 | Name = "${var.name}-RT" 6 | } 7 | } 8 | 9 | resource "aws_subnet" "persistence" { 10 | vpc_id = var.vpc_id 11 | cidr_block = var.private_persistence_subnets[count.index] 12 | availability_zone = var.azs[count.index] 13 | count = length(var.azs) 14 | 15 | tags { 16 | Name = "${var.name}-${var.azs[count.index]}" 17 | } 18 | } 19 | 20 | resource "aws_route_table_association" "persistence" { 21 | count = length(var.azs) 22 | subnet_id = element(aws_subnet.persistence.*.id, count.index) 23 | route_table_id = aws_route_table.persistence.id 24 | } 25 | -------------------------------------------------------------------------------- /modules/private-app-subnet/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_route_table" "private" { 2 | vpc_id = var.vpc_id 3 | count = length(var.azs) 4 | 5 | route { 6 | cidr_block = "0.0.0.0/0" 7 | nat_gateway_id = element(split(",", var.nat_gateway_ids), count.index) 8 | } 9 | 10 | tags { 11 | Name = "${var.name}-${var.azs[count.index]}" 12 | } 13 | 14 | lifecycle { 15 | create_before_destroy = true 16 | } 17 | } 18 | 19 | resource "aws_subnet" "private" { 20 | vpc_id = var.vpc_id 21 | cidr_block = var.private_app_subnets[count.index] 22 | availability_zone = var.azs[count.index] 23 | count = length(var.azs) 24 | 25 | tags { 26 | Name = "${var.name}-${var.azs[count.index]}" 27 | } 28 | 29 | lifecycle { 30 | create_before_destroy = true 31 | } 32 | } 33 | 34 | resource "aws_route_table_association" "private" { 35 | count = length(var.azs) 36 | subnet_id = element(aws_subnet.private.*.id, count.index) 37 | route_table_id = element(aws_route_table.private.*.id, count.index) 38 | } 39 | -------------------------------------------------------------------------------- /modules/public-subnet/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_internet_gateway" "public" { 2 | vpc_id = var.vpc_id 3 | 4 | tags { 5 | Name = "${var.name}-IG" 6 | } 7 | } 8 | 9 | resource "aws_route_table" "public" { 10 | vpc_id = var.vpc_id 11 | 12 | route { 13 | cidr_block = "0.0.0.0/0" 14 | gateway_id = aws_internet_gateway.public.id 15 | } 16 | 17 | tags { 18 | Name = "${var.name}-rt" 19 | } 20 | } 21 | 22 | resource "aws_subnet" "public" { 23 | vpc_id = var.vpc_id 24 | cidr_block = var.public_subnets[count.index] 25 | availability_zone = var.azs[count.index] 26 | count = length(var.azs) 27 | 28 | tags { 29 | Name = "${var.name}-${var.azs[count.index]}" 30 | } 31 | 32 | lifecycle { 33 | create_before_destroy = true 34 | } 35 | 36 | map_public_ip_on_launch = true 37 | } 38 | 39 | resource "aws_route_table_association" "public" { 40 | count = length(var.azs) 41 | subnet_id = element(aws_subnet.public.*.id, count.index) 42 | route_table_id = aws_route_table.public.id 43 | } 44 | -------------------------------------------------------------------------------- /modules/bastion/iam.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_instance_profile" "s3_readonly" { 2 | name = "${var.name}-s3-readonly" 3 | role = aws_iam_role.s3_readonly.name 4 | } 5 | 6 | resource "aws_iam_role" "s3_readonly" { 7 | name = "${var.name}-s3-readonly-role" 8 | path = "/" 9 | 10 | assume_role_policy = < 0 ? 1 : 0 3 | peer_owner_id = var.peer_owner_id 4 | peer_vpc_id = var.peer_vpc_id 5 | vpc_id = var.root_vpc_id 6 | auto_accept = "true" 7 | 8 | tags { 9 | Name = "${var.name}-px" 10 | } 11 | } 12 | 13 | resource "aws_route" "root_to_peer_private_route" { 14 | count = length(var.peer_vpc_id) > 0 ? var.root_route_table_ids_count : 0 15 | route_table_id = element(split(",", var.root_route_table_ids), count.index) 16 | destination_cidr_block = var.peer_vpc_cidr 17 | vpc_peering_connection_id = aws_vpc_peering_connection.vpc_peering_connection.id 18 | } 19 | 20 | resource "aws_route" "peer_to_root_private_route" { 21 | count = length(var.peer_vpc_id) > 0 ? var.root_route_table_ids_count : 0 # https://github.com/hashicorp/terraform/issues/3888" 22 | route_table_id = element(split(",", var.peer_route_table_ids), count.index) 23 | destination_cidr_block = var.root_vpc_cidr 24 | vpc_peering_connection_id = aws_vpc_peering_connection.vpc_peering_connection.id 25 | } 26 | -------------------------------------------------------------------------------- /modules/vpc-peering/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" {} 2 | variable "root_vpc_id" {} 3 | 4 | variable "root_vpc_cidr" { 5 | description = "VPC CIDR of the root VPC" 6 | } 7 | 8 | variable "root_route_table_ids" { 9 | description = "List of Route Table IDs of the root vpc" 10 | } 11 | 12 | variable "root_route_table_ids_count" { 13 | description = "count for root_route_table_ids list (https://github.com/hashicorp/terraform/issues/3888)" 14 | default = "1" # Default value is a must: https://github.com/hashicorp/terraform/issues/8146 15 | } 16 | 17 | variable "peer_vpc_id" {} 18 | 19 | variable "peer_owner_id" { 20 | description = "AWS account ID of the account through which the peer vpc was created" 21 | } 22 | 23 | variable "peer_vpc_cidr" { 24 | description = "VPC CIDR of the peer VPC" 25 | } 26 | 27 | variable "peer_route_table_ids" { 28 | description = "Comma-separated list of Route Table IDs of the peer vpc" 29 | } 30 | 31 | variable "peer_route_table_ids_count" { 32 | description = "Count for peer_route_table_ids list (https://github.com/hashicorp/terraform/issues/3888)" 33 | default = "1" # Default value is a must: https://github.com/hashicorp/terraform/issues/8146 34 | } 35 | -------------------------------------------------------------------------------- /modules/bastion/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" {} 2 | variable "vpc_id" {} 3 | variable "ami" {} 4 | 5 | variable "name" { 6 | default = "bastion-host" 7 | } 8 | 9 | variable "keypair" { 10 | default = "bastion-host" 11 | } 12 | 13 | variable "instance_type" { 14 | default = "t2.nano" 15 | } 16 | 17 | variable "s3_bucket_name" { 18 | default = "" # Default value is a must: https://github.com/hashicorp/terraform/issues/8146 19 | } 20 | 21 | variable "s3_bucket_arn" { 22 | default = "" 23 | } 24 | 25 | variable "s3_bucket_uri" { 26 | default = "" 27 | } 28 | 29 | variable "ssh_user" { 30 | default = "ubuntu" 31 | } 32 | 33 | variable "enable_hourly_cron_updates" { 34 | default = "false" 35 | } 36 | 37 | variable "keys_update_frequency" { 38 | default = "" 39 | } 40 | 41 | variable "additional_user_data_script" { 42 | default = "" 43 | } 44 | 45 | variable "security_group_ids" { 46 | description = "Comma seperated list of security groups to apply to the bastion." 47 | default = "" 48 | } 49 | 50 | variable "subnet_ids" { 51 | description = "Comma separated list of subnet ids" 52 | } 53 | 54 | variable "associate_public_ip_address" { 55 | description = "Associate a public ip address with an instance in a VPC." 56 | default = true 57 | } 58 | 59 | variable "allow_ssh_cidrs" { 60 | description = "List Cidrs from where ssh is to be allowed for bastion host. Default is anywhere" 61 | type = list 62 | default = ["0.0.0.0/0"] 63 | } 64 | 65 | variable "eip" { 66 | default = "" 67 | } 68 | -------------------------------------------------------------------------------- /modules/vpc-peering/README.md: -------------------------------------------------------------------------------- 1 | # vpc-peerimg 2 | 3 | This Terraform Module creates [VPC Peering 4 | Connections](http://docs.aws.amazon.com/AmazonVPC/latest/PeeringGuide/Welcome.html) between VPCs. Normally, VPCs are completely isolated from each other, but sometimes, you want to allow traffic to flow between them, such as allowing DevOps tools running in a *Global Admiral Management VPC* to talk to apps running in a *Stage* or *Prod VPC*. This module can create peering connections and route table entries that make this sort of cross-VPC communication possible. 5 | 6 | ## What's a VPC? 7 | 8 | A [VPC](https://aws.amazon.com/vpc/) or Virtual Private Cloud is a logically isolated section of your AWS cloud. Each VPC defines a virtual network within which you run your AWS resources, as well as rules for what can go in and out of that network. This includes subnets, route tables that tell those subnets how to route inbound and outbound traffic, security groups, firewalls for the subnet (known as "Network ACLs"), and any other network components such as VPN connections. 9 | 10 | ## Why bother with peering and not just put everything in one VPC? 11 | 12 | We intentionally keep VPCs as isolated as we can to reduce the chances that a problem in one VPC will affect the other VPCs. For example, our standard VPC deployment gives you an isolated staging VPC where you can test changes without having to worry that they might affect production. Similarly, if an attacker breaks into the staging VPC, they cannot easily access your production data without breaking through yet another layer of security. These multiple layers are known as "defense-in-depth." 13 | 14 | The point of VPC peering is to allow limited, controlled cross-VPC communication. In particular, you may want to set up peering to allow a user logged into a management VPC to carry out maintenance tasks in the staging and production VPCs. However, VPC peering relationships are not "transitive": even though the management VPC can access both staging and production, someone in staging *cannot* access production. -------------------------------------------------------------------------------- /modules/bastion/user-data/user-data.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############## 4 | # Install deps 5 | ############## 6 | # Ubuntu 7 | apt-get update 8 | apt-get install python-pip jq -y 9 | ##################### 10 | 11 | # Amazon Linux (RHEL) - NAT instances 12 | yum update 13 | yum install python-pip jq -y 14 | ##################### 15 | 16 | pip install --upgrade awscli 17 | 18 | ############## 19 | 20 | cat <<"EOF" > /home/${ssh_user}/update_ssh_authorized_keys.sh 21 | #!/usr/bin/env bash 22 | set -e 23 | BUCKET_NAME=${s3_bucket_name} 24 | BUCKET_URI=${s3_bucket_uri} 25 | SSH_USER=${ssh_user} 26 | MARKER="# KEYS_BELOW_WILL_BE_UPDATED_BY_TERRAFORM" 27 | KEYS_FILE=/home/$SSH_USER/.ssh/authorized_keys 28 | TEMP_KEYS_FILE=$(mktemp /tmp/authorized_keys.XXXXXX) 29 | PUB_KEYS_DIR=/home/$SSH_USER/pub_key_files/ 30 | [[ -z $BUCKET_URI ]] && BUCKET_URI="s3://$BUCKET_NAME/" 31 | mkdir -p $PUB_KEYS_DIR 32 | # Add marker, if not present, and copy static content. 33 | grep -Fxq "$MARKER" $KEYS_FILE || echo -e "\n$MARKER" >> $KEYS_FILE 34 | line=$(grep -n "$MARKER" $KEYS_FILE | cut -d ":" -f 1) 35 | head -n $line $KEYS_FILE > $TEMP_KEYS_FILE 36 | # Synchronize the keys from the bucket. 37 | aws s3 sync --delete $BUCKET_URI $PUB_KEYS_DIR 38 | for filename in $PUB_KEYS_DIR/*; do 39 | sed 's/\n\?$/\n/' < $filename >> $TEMP_KEYS_FILE 40 | done 41 | # Move the new authorized keys in place. 42 | chown $SSH_USER:$SSH_USER $KEYS_FILE 43 | chmod 600 $KEYS_FILE 44 | mv $TEMP_KEYS_FILE $KEYS_FILE 45 | EOF 46 | 47 | chown ${ssh_user}:${ssh_user} /home/${ssh_user}/update_ssh_authorized_keys.sh 48 | chmod 755 /home/${ssh_user}/update_ssh_authorized_keys.sh 49 | 50 | # Execute now 51 | su ${ssh_user} -c /home/${ssh_user}/update_ssh_authorized_keys.sh 52 | 53 | # Be backwards compatible with old cron update enabler 54 | if [ "${enable_hourly_cron_updates}" = 'true' -a -z "${keys_update_frequency}" ]; then 55 | keys_update_frequency="0 * * * *" 56 | else 57 | keys_update_frequency="${keys_update_frequency}" 58 | fi 59 | 60 | # Add to cron 61 | if [ -n "$keys_update_frequency" ]; then 62 | croncmd="/home/${ssh_user}/update_ssh_authorized_keys.sh" 63 | cronjob="$keys_update_frequency $croncmd" 64 | ( crontab -u ${ssh_user} -l | grep -v "$croncmd" ; echo "$cronjob" ) | crontab -u ${ssh_user} - 65 | fi 66 | 67 | # Append addition user-data script 68 | ${additional_user_data_script} -------------------------------------------------------------------------------- /modules/bastion/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_security_group" "bastion" { 2 | name = var.name 3 | vpc_id = var.vpc_id 4 | description = "Bastion security group (only SSH inbound access is allowed)" 5 | 6 | tags { 7 | Name = var.name 8 | } 9 | 10 | ingress { 11 | protocol = "tcp" 12 | from_port = 22 13 | to_port = 22 14 | 15 | cidr_blocks = var.allow_ssh_cidrs 16 | } 17 | 18 | egress { 19 | protocol = -1 20 | from_port = 0 21 | to_port = 0 22 | 23 | cidr_blocks = [ 24 | "0.0.0.0/0", 25 | ] 26 | } 27 | 28 | lifecycle { 29 | create_before_destroy = true 30 | } 31 | } 32 | 33 | data "template_file" "user_data" { 34 | template = file("${path.module}/user-data/user-data.sh") 35 | 36 | vars = { 37 | s3_bucket_name = var.s3_bucket_name 38 | s3_bucket_uri = var.s3_bucket_uri 39 | ssh_user = var.ssh_user 40 | keys_update_frequency = var.keys_update_frequency 41 | enable_hourly_cron_updates = var.enable_hourly_cron_updates 42 | additional_user_data_script = var.additional_user_data_script 43 | } 44 | } 45 | 46 | resource "aws_launch_configuration" "bastion" { 47 | name_prefix = var.name 48 | image_id = var.ami 49 | instance_type = var.instance_type 50 | key_name = var.keypair 51 | user_data = data.template_file.user_data.rendered 52 | associate_public_ip_address = var.associate_public_ip_address 53 | 54 | security_groups = [ 55 | compact(concat(list(aws_security_group.bastion.id), split(",", var.security_group_ids))), 56 | ] 57 | 58 | iam_instance_profile = aws_iam_instance_profile.s3_readonly.name 59 | 60 | lifecycle { 61 | create_before_destroy = true 62 | } 63 | } 64 | 65 | resource "aws_autoscaling_group" "bastion" { 66 | name = var.name 67 | vpc_zone_identifier = [split(",", var.subnet_ids)] 68 | desired_capacity = "1" 69 | min_size = "1" 70 | max_size = "1" 71 | health_check_grace_period = "60" 72 | health_check_type = "EC2" 73 | force_delete = false 74 | wait_for_capacity_timeout = 0 75 | launch_configuration = aws_launch_configuration.bastion.name 76 | 77 | enabled_metrics = [ 78 | "GroupMinSize", 79 | "GroupMaxSize", 80 | "GroupDesiredCapacity", 81 | "GroupInServiceInstances", 82 | "GroupPendingInstances", 83 | "GroupStandbyInstances", 84 | "GroupTerminatingInstances", 85 | "GroupTotalInstances", 86 | ] 87 | 88 | tag { 89 | key = "Name" 90 | value = var.name 91 | propagate_at_launch = true 92 | } 93 | 94 | tag { 95 | key = "EIP" 96 | value = var.eip 97 | propagate_at_launch = true 98 | } 99 | 100 | lifecycle { 101 | create_before_destroy = true 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /modules/network.tf: -------------------------------------------------------------------------------- 1 | ###################### 2 | # VARIABLES 3 | ###################### 4 | variable "name" {} 5 | 6 | variable "vpc_cidr" {} 7 | variable "aws_region" {} 8 | 9 | variable "azs" { 10 | description = "A list of Availability zones in the region" 11 | default = [] 12 | } 13 | 14 | variable "private_app_subnets" { 15 | description = "A list of CIDR blocks for private app subnets inside the VPC." 16 | default = [] 17 | } 18 | 19 | variable "public_subnets" { 20 | description = "A list of CIDR blocks for public subnets inside the VPC." 21 | default = [] 22 | } 23 | 24 | variable "private_persistence_subnets" { 25 | description = "A list of CIDR blocks for private persistence subnets inside the VPC." 26 | default = [] 27 | } 28 | 29 | variable "spare_subnets" { 30 | description = "A list of CIDR blocks for spare subnets inside the VPC." 31 | default = [] 32 | } 33 | 34 | ## VPC-Peering variables 35 | # Default values assigned inorder to mark them optional 36 | variable "peer_vpc_id" { 37 | default = "" 38 | } 39 | 40 | variable "peer_vpc_cidr" { 41 | default = "0.0.0.0/0" 42 | } 43 | 44 | variable "peer_private_app_route_table_ids" { 45 | default = " " 46 | } 47 | 48 | variable "peer_owner_id" { 49 | description = "AWS Account ID of the owner of the peer VPC" 50 | default = " " 51 | } 52 | 53 | ## Bastion Host variables 54 | # Default values assigned inorder to mark them optional 55 | variable "config_bucket_name" { 56 | description = "Name of S3 Bucket which has config and keys to be accessed by the bastion host" 57 | } 58 | 59 | variable "config_bucket_arn" { 60 | description = "ARN of S3 Bucket which has config and keys to be accessed by the bastion host" 61 | } 62 | 63 | variable "bastion_host_keypair" { 64 | description = "Keypair name for the bastion-host instance" 65 | default = "bastion-host" 66 | } 67 | 68 | variable "bastion_host_allow_ssh_cidrs" { 69 | description = "List Cidrs from where ssh is to be allowed for bastion host. Default is anywhere" 70 | type = list 71 | default = ["0.0.0.0/0"] 72 | } 73 | 74 | variable "bastion_host_ami_id" { 75 | description = "AMI ID from which the bastian host instance will be created." 76 | default = "" 77 | } 78 | 79 | ###################### 80 | # MODULES 81 | ###################### 82 | module "vpc" { 83 | source = "./vpc" 84 | 85 | name = "${var.name}-vpc" 86 | vpc_cidr = var.vpc_cidr 87 | } 88 | 89 | module "public_subnet" { 90 | source = "./public-subnet" 91 | 92 | name = "${var.name}-public" 93 | vpc_id = module.vpc.vpc_id 94 | public_subnets = var.public_subnets 95 | azs = var.azs 96 | } 97 | 98 | module "nat" { 99 | source = "./nat" 100 | 101 | name = "${var.name}-nat" 102 | azs = var.azs 103 | public_subnet_ids = module.public_subnet.subnet_ids 104 | } 105 | 106 | module "private_app_subnet" { 107 | source = "./private-app-subnet" 108 | 109 | name = "${var.name}-private-app" 110 | vpc_id = module.vpc.vpc_id 111 | private_app_subnets = var.private_app_subnets 112 | azs = var.azs 113 | 114 | nat_gateway_ids = module.nat.nat_gateway_ids 115 | } 116 | 117 | module "private_persistence_subnet" { 118 | source = "./private-persistence-subnet" 119 | 120 | name = "${var.name}-private-persistence" 121 | vpc_id = module.vpc.vpc_id 122 | private_persistence_subnets = var.private_persistence_subnets 123 | azs = var.azs 124 | } 125 | 126 | module "spare_subnet" { 127 | source = "./spare-subnet" 128 | 129 | name = "${var.name}-spare" 130 | vpc_id = module.vpc.vpc_id 131 | spare_subnets = var.spare_subnets 132 | azs = var.azs 133 | } 134 | 135 | module "network_acl" { 136 | source = "./network-acl" 137 | 138 | name = "${var.name}-acl" 139 | vpc_id = module.vpc.vpc_id 140 | public_subnet_ids = module.public_subnet.subnet_ids 141 | private_app_subnet_ids = module.private_app_subnet.subnet_ids 142 | } 143 | 144 | ##### VPC-PEERING 145 | module "vpc-peering" { 146 | source = "./vpc-peering" 147 | name = "${var.name}-vpc-peering" 148 | 149 | root_vpc_id = module.vpc.vpc_id 150 | root_vpc_cidr = module.vpc.vpc_cidr 151 | root_route_table_ids = module.private_app_subnet.route_table_ids 152 | 153 | # workaround: using number of availability_zones for the number of routes to be added in the route table 154 | # https://github.com/hashicorp/terraform/issues/3888 155 | root_route_table_ids_count = length(var.azs) 156 | 157 | peer_route_table_ids = var.peer_private_app_route_table_ids 158 | 159 | # workaround: using number of availability_zones for the number of routes to be added in the route table 160 | # https://github.com/hashicorp/terraform/issues/3888 161 | peer_route_table_ids_count = length(var.azs) 162 | 163 | peer_owner_id = var.peer_owner_id 164 | peer_vpc_id = var.peer_vpc_id 165 | peer_vpc_cidr = var.peer_vpc_cidr 166 | } 167 | 168 | module "bastion-host" { 169 | name = "${var.name}-bastion-host" 170 | source = "./bastion" 171 | instance_type = "t2.nano" 172 | keypair = var.bastion_host_keypair 173 | allow_ssh_cidrs = var.bastion_host_allow_ssh_cidrs 174 | ami = var.bastion_host_ami_id 175 | region = var.aws_region 176 | s3_bucket_uri = "s3://${var.config_bucket_name}/keypairs" 177 | s3_bucket_arn = var.config_bucket_arn 178 | vpc_id = module.vpc.vpc_id 179 | subnet_ids = module.public_subnet.subnet_ids 180 | keys_update_frequency = "5,20,35,50 * * * *" 181 | additional_user_data_script = "date" 182 | } 183 | 184 | ###################### 185 | # OUTPUTS 186 | ###################### 187 | # VPC 188 | output "vpc_id" { 189 | value = module.vpc.vpc_id 190 | } 191 | 192 | output "vpc_cidr" { 193 | value = module.vpc.vpc_cidr 194 | } 195 | 196 | # Subnets 197 | output "public_subnet_ids" { 198 | value = module.public_subnet.subnet_ids 199 | } 200 | 201 | output "private_app_subnet_ids" { 202 | value = module.private_app_subnet.subnet_ids 203 | } 204 | 205 | output "private_persistence_subnet_ids" { 206 | value = module.private_persistence_subnet.subnet_ids 207 | } 208 | 209 | # Route Tables 210 | output "private_app_route_table_ids" { 211 | value = module.private_app_subnet.route_table_ids 212 | } 213 | 214 | output "public_route_table_ids" { 215 | value = module.public_subnet.route_table_ids 216 | } 217 | 218 | # NAT 219 | output "nat_gateway_ids" { 220 | value = module.nat.nat_gateway_ids 221 | } 222 | 223 | output "nat_public_ip" { 224 | value = module.nat.public_ip 225 | } 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # blueprint-network-aws 2 | 3 | This *blueprint* contains modules that are used to set up a best practices network topology in your AWS account. This *blueprint* creates an isolated VPC for each environment (staging, production, mgmt), and within each environment, sets up multiple tiers of isolated subnets (public, private, persistence) network ACLs, security groups, NAT gateways, bastion host, and VPC peering connections. 4 | 5 | - Network 6 | - VPC 7 | - Public subnets 8 | - Private subnets 9 | _ Private persistence subnets 10 | - NAT 11 | - OpenVPN 12 | - Bastion host 13 | 14 | ## What is a blueprint? 15 | 16 | At Stakater, we've taken the thousands of hours we spent building infrastructure on AWS and condensed all that experience and code into pre-built blueprints or packages or modules. Each blueprint is a battle-tested, best-practices definition of a piece of infrastructure, such as a VPC, ECS cluster, or an Auto Scaling Group. Modules are versioned using Semantic Versioning to allow Stakater clients to keep up to date with the latest infrastructure best practices in a systematic way. 17 | 18 | ## Concepts 19 | 20 | ### What's a VPC? 21 | 22 | A [VPC](https://aws.amazon.com/vpc/) or Virtual Private Cloud is a logically isolated section of your AWS cloud. Each 23 | VPC defines a virtual network within which you run your AWS resources, as well as rules for what can go in and out of 24 | that network. This includes subnets, route tables that tell those subnets how to route inbound and outbound traffic, 25 | security groups, firewalls for the subnet (known as "Network ACLs"), and any other network components such as VPN connections. 26 | 27 | #### Benefits of a VPC 28 | 29 | Before VPCs existed in AWS, every EC2 Instance launched in AWS was addressable by the public Internet, or any other EC2 30 | Instance launched in AWS, even from different customers! You could block network access using security groups or OS-managed 31 | firewalls, but this still represented a security step backward from traditional data center setups where a given server would be physically unreachable from the Internet. 32 | 33 | VPCs are fundamentally about isolating your resources so that they're only reachable by a limited set of other resources 34 | you define. You can set granular isolation rules by defining Route Tables for each Subnet. You can allow a limited set 35 | of outsiders to connect to your VPC, for example, using VPN, or just by exposing a single host accessible to the public. 36 | 37 | The general point is that you have an isolated environment you can use to lock down access. 38 | 39 | Given all the above, an intuitive way to leverage a VPC is to make each VPC represent a unique environment by having, 40 | for example, a prod VPC and stage VPC. 41 | 42 | ### CIDR-Formatted IP Address Ranges 43 | 44 | Because a VPC is an isolated world meant specially for your use, you can define a range of private IP addresses that the VPC 45 | will allow. For example, we may wish to allow any IP address from 10.0.50.0 to 10.0.50.15. 46 | 47 | But we need a more concise way to represent such an IP address range, and the de facto standard is the Classless Inter- 48 | Domain Routing (CIDR) standard. The name is confusing but as [Wikipedia](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) explains, the concept works as follows: 49 | 50 | 1. Convert a base-10 IP like `10.0.50.0` to binary format: `00001010.00000000.00110010.00000000` 51 | 52 | 2. Decide how many binary digits (or bits) we will allow to "vary." For example, suppose I want the following range of IP 53 | addresses: `00001010.00000000.00110010.00000000` to `00001010.00000000.00110010.11111111` (`10.0.50.0` to `10.0.50.255`). 54 | Notice that the first three "octets" (group of 8 bits) are the same, but the last octet ranges from 0 to 255. 55 | 56 | 3. Express the number of varying bits in CIDR format: `/`. For 57 | example, if we use the range in the previous step, we'd have `10.0.50.0/24`. The first 24 bits are fixed so that the 58 | remaining 8 bits can vary. In CIDR parlance, our "IP Address" is `10.0.50.0` and our "Network Mask" is `24`. 59 | 60 | Sometimes CIDR Ranges are called CIDR Blocks. The CIDR Block `0.0.0.0/0` corresponds to any IP address. The CIDR Block 61 | `1.2.3.4/32` corresponds to only `1.2.3.4`. 62 | 63 | You'll notice that every VPC has a CIDR Block, and indeed this represents the range of private IP addresses 64 | which can be assigned to resources in the VPC. 65 | 66 | ### Subnets 67 | 68 | Subnets are "sub-networks", or a partition of the VPC. For example, a VPC might have the CIDR range `10.0.50.0/24` 69 | (`10.0.15.0` - `10.0.15.255`) and a subnet might allow just IP addresses in the range `10.0.50.0/28` (`10.0.15.0` - 70 | `10.0.15.16`). Note that subnets cannot have overlapping CIDR Ranges. 71 | 72 | In addition, each subnet can have a unique Route Table. 73 | 74 | ### Route Tables 75 | 76 | Each subnet needs a [Route Table](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Route_Tables.html) so that 77 | it knows how to route traffic within that subnet. For example, a given subnet might route traffic destined for CIDR Block 78 | `10.0.20.0/24` to a VPC Peering Connection, traffic for `10.0.10.0/24` within the VPC, and all the rest (`0.0.0.0/0`) to 79 | to the Internet Gateway so it can reach the public Internet. The Route Table declares all this. 80 | 81 | ### The Internet Gateway 82 | 83 | The best way to think of an [Internet Gateway](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Internet_Gateway.html) 84 | is that it's the destination that VPC traffic destined for the public Internet gets routed to. This configuration is 85 | recorded in a Route Table. 86 | 87 | ### NAT Gateways 88 | 89 | If you launch an EC2 Instance in one of the **Public Subnets** defined above, it will automatically be addressable from 90 | the public Internet and have outbound Internet access itself. 91 | 92 | But if you launch an EC2 Instance in one of the **Private Subnets** defined above, it will NOT be addressable from the 93 | public Internet. This is a useful security property. For example, we generally don't want our databases directly addressable 94 | on the public Internet. 95 | 96 | But what if an EC2 Instance in a private subnet needs *outbound* Internet access? It could route its requests to the 97 | Internet, but there's no way for the Internet to return the response since, as we just explained, the EC2 Instance isn't 98 | addressable from the Internet. 99 | 100 | To solve this problem, we need our private EC2 Instance to submit its public Internet requests through another EC2 Instance 101 | that's located in a public subnet. That EC2 Instance should keep track of where it got its original request so that it 102 | can redirect or "translate" the response it receives back to the original requestor. 103 | 104 | Such an EC2 Instance is called a "Network Address Translation" instance, or NAT instance. 105 | 106 | But what if the NAT Instance goes down? Now our private EC2 Instance can't reach the Internet at all. That's why it's 107 | preferable to have a highly available NAT Instance service, and that's what Amazon's [NAT Gateway](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-nat-gateway.html) 108 | service is. Amazon runs more than one EC2 Instance behind the scenes, and automatically handles failover if one instance dies. 109 | 110 | ### VPC Endpoints 111 | 112 | By default, when an EC2 Instance makes an AWS API call, that HTTPS request is still routed through the public Internet. 113 | AWS customers complained that they didn't want their AWS API requests traveling outside the VPC, so AWS released a 114 | [VPC Endpoint](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-endpoints.html) service. 115 | 116 | VPC Endpoints cost nothing and provide a new destination for a Route Table so that when certain AWS API requests are made 117 | instead of being routed to the public AWS API endpoint, they are routed directly within the VPC. Unfortunately, as of 118 | June 10, 2016, the only AWS service supported for VPC Endpoints is S3. 119 | 120 | ### What's a Network ACL? 121 | 122 | [Network ACLs](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_ACLs.html) provide an extra layer of network 123 | security, similar to a [security group](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-network-security.html). 124 | Whereas a security group controls what inbound and outbound traffic is allowed for a specific resource (e.g. a single 125 | EC2 instance), a network ACL controls what inbound and outbound traffic is allowed for an entire subnet. 126 | 127 | ### What's a bastion host? 128 | 129 | 130 | ### What's VPC Peering? 131 | 132 | 133 | ### Networking 134 | 135 | ![](./diagrams/networking.png) 136 | 137 | By default, the Stackater will create a VPC in a single region, amongst multiple availability zones (AZs). The default mask for this VPC is 138 | 139 | 10.30.0.0/16 140 | 141 | The address was chosen to be internal, and to not conflict with other pieces of infrastructure you might run. But, it can also be configured with its own CIDR range. 142 | 143 | Each availability zone will get its own external and internal subnets. Most of our infrastructure will live in the *internal* subnet so that they are not externally accessible to the internet. 144 | 145 | If you'd like to scale to multiple regions, simply add one to the second octet. 146 | 147 | 10.31.0.0/16 -- my new region 148 | 149 | To span across availability zones, the regional 16-bit mask becomes 18-bits. 150 | 151 | 10.30.0.0/18 - AZ A 152 | 10.30.64.0/18 - AZ B 153 | 10.30.128.0/18 - AZ C 154 | 10.30.192.0/18 - Spare 155 | 156 | To subdivide each availability zone into spaces for internal, external and to have spare room for growth; use a 19-bit mask for internal, and a 20-bit mask for external. The external space is smaller because only a few instances and load-balancers should be provisioned into it. 157 | 158 | 10.30.0.0/18 - AZ A 159 | 160 | 10.30.0.0/19 internal 161 | 10.30.32.0/20 external 162 | 10.30.48.0/20 spare 163 | 164 | 10.30.64.0/18 - AZ B 165 | 166 | 10.30.64.0/19 internal 167 | 10.30.96.0/20 external 168 | 10.30.112.0/20 spare 169 | 170 | 10.30.128.0/18 - AZ C 171 | 172 | 10.30.128.0/19 internal 173 | 10.30.160.0/20 external 174 | 10.30.176.0/20 spare 175 | 176 | The VPC itself will contain a single network gateway to route traffic in and out of the different subnets. The Stackater terraform will automatically create separate [NAT Gateways][nat-gateway] in each of the different subnets. 177 | 178 | Traffic from each internal subnet to the outside world will run through the associated NAT gateway. 179 | 180 | For further reading, check out these sources: 181 | 182 | - [Recommended Address Space](http://serverfault.com/questions/630022/what-is-the-recommended-cidr-when-creating-vpc-on-aws) 183 | - [Practical VPC Design](https://medium.com/aws-activate-startup-blog/practical-vpc-design-8412e1a18dcc) 184 | - [nat-gateway](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-nat-gateway.html) 185 | 186 | ## Developing a module 187 | 188 | ### Versioning 189 | 190 | We are following the principles of [Semantic Versioning](http://semver.org/). During initial development, the major 191 | version is to 0 (e.g., `0.x.y`), which indicates the code does not yet have a stable API. Once we hit `1.0.0`, we will 192 | follow these rules: 193 | 194 | 1. Increment the patch version for backwards-compatible bug fixes (e.g., `v1.0.8 -> v1.0.9`). 195 | 2. Increment the minor version for new features that are backwards-compatible (e.g., `v1.0.8 -> 1.1.0`). 196 | 3. Increment the major version for any backwards-incompatible changes (e.g. `1.0.8 -> 2.0.0`). 197 | 198 | The version is defined using Git tags. Use GitHub to create a release, which will have the effect of adding a git tag. 199 | 200 | ## Credits 201 | 202 | 1. https://github.com/terraform-community-modules/tf_aws_vpc 203 | 2. https://github.com/gruntwork-io/module-vpc-public 204 | 3. https://github.com/hashicorp/best-practices/tree/master/terraform/modules/aws/network 205 | 4. https://github.com/hashicorp/atlas-examples/tree/master/infrastructures 206 | 207 | 208 | #### NOTE: 209 | Any changes made to this repository should also be reflected in its respective clone for kubernetes (https://github.com/stakater/blueprint-network-k8s-aws). 210 | Both repositories should contain same code with the difference of kubernetes specific tags on resources of the clone repositroy `blueprint-network-k8s-aws`. 211 | --------------------------------------------------------------------------------