From b8862faaaec88c9003e4f769a103a040769b10ca Mon Sep 17 00:00:00 2001 From: Mihai Vultur Date: Mon, 24 Apr 2017 09:41:00 +0000 Subject: [PATCH] Version 1.4.0 Separate NAT instance configuration using a 'terraform module'. --- DevOpsVPC/.gitignore => .gitignore | 3 ++ CHANGELOG.md | 3 ++ DevOpsVPC/Makefile | 3 +- DevOpsVPC/infrastructure.conf | 5 ++ DevOpsVPC/instances.tf | 77 +++++++++--------------------- DevOpsVPC/outputs.tf | 6 +-- DevOpsVPC/security.tf | 40 ++++++++++++---- DevOpsVPC/variables.tf | 10 ++++ DevOpsVPC/vpc.tf | 2 +- README.md | 38 +++++++++++++-- modules/nat/eip.tf | 22 +++++++++ modules/nat/main.tf | 36 ++++++++++++++ modules/nat/output.tf | 30 ++++++++++++ modules/nat/remote-exec.tf | 38 +++++++++++++++ modules/nat/security.tf | 31 ++++++++++++ modules/nat/variables.tf | 62 ++++++++++++++++++++++++ 16 files changed, 335 insertions(+), 71 deletions(-) rename DevOpsVPC/.gitignore => .gitignore (55%) create mode 100644 modules/nat/eip.tf create mode 100644 modules/nat/main.tf create mode 100644 modules/nat/output.tf create mode 100644 modules/nat/remote-exec.tf create mode 100644 modules/nat/security.tf create mode 100644 modules/nat/variables.tf diff --git a/DevOpsVPC/.gitignore b/.gitignore similarity index 55% rename from DevOpsVPC/.gitignore rename to .gitignore index 87fa0e9..0974d84 100644 --- a/DevOpsVPC/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ terraform.tfstate *.bak *~ .*.swp +# statefiles/ -- is difficult to store them in SCM, +# better configure terraform remote state storage +statefiles/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 523d76e..95ec2e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ CHANGELOG This file is used to list changes made in each version of the `en_infra_aws` project. +### Version 1.4.0 +Separate NAT instance configuration using a 'terraform module'. + ### Version 1.3.0 Move terraform generated files: (.tfstate, .tfplan) into a separate `statefiles` directory. diff --git a/DevOpsVPC/Makefile b/DevOpsVPC/Makefile index 986055d..dd31a0c 100644 --- a/DevOpsVPC/Makefile +++ b/DevOpsVPC/Makefile @@ -33,4 +33,5 @@ apply: destroy: terraform plan -destroy -var-file infrastructure.conf -out=statefiles/destroy.tfplan -state=statefiles/terraform.tfstate | tee -a logs/destroy.log terraform apply statefiles/destroy.tfplan | tee -a logs/destroy.log - mv -f terraform.tfstate statefiles/destroy.tfstate + mv -f statefiles/terraform.tfstate statefiles/terraform.tfstate.old + mv -f terraform.tfstate statefiles/terraform.tfstate diff --git a/DevOpsVPC/infrastructure.conf b/DevOpsVPC/infrastructure.conf index 48fa51a..9d461e2 100755 --- a/DevOpsVPC/infrastructure.conf +++ b/DevOpsVPC/infrastructure.conf @@ -35,5 +35,10 @@ vpc_public_subnets = ["10.10.10.0/24"] ec2_custom_image = "" # ami-bcd95caa is the ubuntu 16.04 ami for 'us-east-1' #-- otherwise ami is determined automatically ec2_os = "CentOS Linux 7 x86_64" +#-- ssh user used to configure instance +ssh_user = "centos" ssh_public_key_name = "terraform-ssh-key" ssh_public_key_file = "~/.ssh/id_rsa.pub" +#-- ports open on the NAT instance +#-- becase we will also have VPN, we also open '45654' +nat_inbound_ports = "22,45654" diff --git a/DevOpsVPC/instances.tf b/DevOpsVPC/instances.tf index 7e9b630..72a1cff 100644 --- a/DevOpsVPC/instances.tf +++ b/DevOpsVPC/instances.tf @@ -11,68 +11,35 @@ # Provisions AWS instances used in our project # - -#-- EIP for NAT Instance -resource "aws_eip" "NatInstance" { - instance = "${aws_instance.NatInstance.id}" - vpc = true -} -#-- -resource "aws_eip_association" "eip_assoc_NatInstance" { - instance_id = "${aws_instance.NatInstance.id}" - allocation_id = "${aws_eip.NatInstance.id}" -} - ################## NAT INSTANCE -resource "aws_instance" "NatInstance" { - ami = "${replace(var.ec2_custom_image, "/^ami-.*/", "1") == 1 ? var.ec2_custom_image : data.aws_ami.centos.image_id}" - availability_zone = "${var.aws_azs[0]}" - instance_type = "t2.micro" - key_name = "${aws_key_pair.xanto.key_name}" - vpc_security_group_ids = ["${aws_security_group.AllowICMP.id}", "${aws_security_group.DefaultPub.id}"] - subnet_id = "${element(aws_subnet.public.*.id, count.index)}" - - source_dest_check = false - - root_block_device { - volume_type = "gp2" - volume_size = 8 - delete_on_termination = true - } - tags = "${merge(var.default_tags, map("VPC", var.vpc_name), map("Name", format("%s_%s", var.vpc_name, "NATInstance")))}" -} - -#-- workarround adding a triggered resource -#-- because of a circular dependency between -#-- eip and aws_instance -resource "null_resource" "preparation" { - triggers { - association_ip_address = "${aws_eip_association.eip_assoc_NatInstance.id}" - } - provisioner "remote-exec" { - inline = [ - "sudo iptables -t nat -C POSTROUTING -o eth0 -s ${join(",", var.vpc_private_subnets)} -j MASQUERADE 2> /dev/null || sudo iptables -t nat -A POSTROUTING -o eth0 -s ${join(",", var.vpc_private_subnets)} -j MASQUERADE", - "echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf", - "echo 'net.ipv4.conf.eth0.send_redirects=0' | sudo tee -a /etc/sysctl.conf", - "echo 'net.netfilter.nf_conntrack_max=131072' | sudo tee -a /etc/sysctl.conf", - "sudo sysctl -p" - ] - connection { - host = "${aws_eip.NatInstance.public_ip}" - user = "centos" - timeout = "30s" - private_key = "${file("/home/vagrant/.ssh/id_rsa")}" - } - } +module "nat" { + source = "../modules/nat" + + number_of_instances = 1 + + instance_name = "NatVPN" + vpc_name = "${var.vpc_name}" + vpc_id = "${aws_vpc.vpc.id}" + instance_type = "t2.micro" + #-- we always want latest CentOS 7 AMI for NAT + ami_id = "${data.aws_ami.centos.image_id}" + subnet_id = "${join(",", aws_subnet.public.*.id)}" + private_subnets_cidr = "${join(",", aws_subnet.private.*.cidr_block)}" + inbound_ports = "${var.nat_inbound_ports}" + user_data = "../files/user-data/nat-vpn.cfg" + key_name = "${aws_key_pair.xanto.key_name}" + #-- assuming 'private key name' by removing the '.pub' extension + private_key_file = "${replace(var.ssh_public_key_file, ".pub", "")}" + sgs = "${aws_default_security_group.default.id},${aws_security_group.AllowICMP.id},${aws_security_group.DefaultPub.id}" } -################## TEST INSTANCE +################### TEST INSTANCE resource "aws_instance" "TestInstance" { ami = "${replace(var.ec2_custom_image, "/^ami-.*/", "1") == 1 ? var.ec2_custom_image : data.aws_ami.centos.image_id}" availability_zone = "${var.aws_azs[0]}" instance_type = "t2.micro" key_name = "${aws_key_pair.xanto.key_name}" - vpc_security_group_ids = ["${aws_security_group.AllowICMP.id}", "${aws_security_group.DefaultPrv.id}"] + vpc_security_group_ids = ["${aws_default_security_group.default.id}", "${aws_security_group.AllowICMP.id}", "${aws_security_group.DefaultPrv.id}"] subnet_id = "${element(aws_subnet.private.*.id, count.index)}" root_block_device { @@ -80,5 +47,5 @@ resource "aws_instance" "TestInstance" { volume_size = 8 delete_on_termination = true } - tags = "${merge(var.default_tags, map("VPC", var.vpc_name), map("Name", format("%s_%s", var.vpc_name, "TestInstance")))}" + tags = "${merge(var.default_tags, map("ManageRunningTime", "WorkingHoursStop"), map("VPC", var.vpc_name), map("Name", format("%s_%s", var.vpc_name, "TestInstance")))}" } diff --git a/DevOpsVPC/outputs.tf b/DevOpsVPC/outputs.tf index ac2d450..183e866 100644 --- a/DevOpsVPC/outputs.tf +++ b/DevOpsVPC/outputs.tf @@ -13,15 +13,15 @@ #-- EC2 Instances output "NAT_instance_id" { - value = "${aws_instance.NatInstance.id}" + value = "${module.nat.instance_id}" } output "NAT_private_ip" { - value = "${aws_instance.NatInstance.private_ip}" + value = "${module.nat.private_ip}" } output "NAT_public_ip" { - value = "${aws_eip.NatInstance.public_ip}" + value = "${module.nat.public_ip}" } #-- diff --git a/DevOpsVPC/security.tf b/DevOpsVPC/security.tf index f6fc3c3..bfe348d 100644 --- a/DevOpsVPC/security.tf +++ b/DevOpsVPC/security.tf @@ -17,6 +17,29 @@ resource "aws_key_pair" "xanto" { public_key = "${file(var.ssh_public_key_file)}" } +#-- Default SG, can't be deleted. Only modified +resource "aws_default_security_group" "default" { + vpc_id = "${aws_vpc.vpc.id}" + + #-- allow traffic between the resources + #-- that have this SG attached + ingress { + protocol = -1 + self = true + from_port = 0 + to_port = 0 + } + + #-- allow outside traffic + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + tags = "${merge(var.default_tags, map("VPC", var.vpc_name), map("Name", format("%s_SG_%s", var.vpc_name, "default")))}" +} + #-- Security Groups resource "aws_security_group" "AllowICMP" { vpc_id = "${aws_vpc.vpc.id}" @@ -53,14 +76,14 @@ resource "aws_security_group" "DefaultPub" { cidr_blocks = ["0.0.0.0/0"] } - # Outbound acces to anywhere - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - +# # Outbound acces to anywhere +# egress { +# from_port = 0 +# to_port = 0 +# protocol = "-1" +# cidr_blocks = ["0.0.0.0/0"] +# } +# tags = "${merge(var.default_tags, map("VPC", var.vpc_name), map("Name", format("%s_SG_%s", var.vpc_name, "DefaultPub")))}" } @@ -87,3 +110,4 @@ resource "aws_security_group" "DefaultPrv" { tags = "${merge(var.default_tags, map("VPC", var.vpc_name), map("Name", format("%s_SG_%s", var.vpc_name, "DefaultPrv")))}" } + diff --git a/DevOpsVPC/variables.tf b/DevOpsVPC/variables.tf index 608d21c..95daeae 100644 --- a/DevOpsVPC/variables.tf +++ b/DevOpsVPC/variables.tf @@ -50,6 +50,11 @@ variable "enable_dns_hostnames" { } #-- Security +variable "ssh_user" { + description = "Name of the ssh user used for connecting to the remote instances" + type = "string" +} + variable "ssh_public_key_name" { description = "Name of the SSH Key" type = "string" @@ -70,6 +75,11 @@ variable "ec2_os" { type = "string" } +#-- +variable "nat_inbound_ports" { + description = "Allow following TCP ports to NAT instance." + default = "22,443" +} ##-- Tags for accounting variable "default_tags" { description = "A map of tags to add to all resources" diff --git a/DevOpsVPC/vpc.tf b/DevOpsVPC/vpc.tf index 9222907..6275793 100644 --- a/DevOpsVPC/vpc.tf +++ b/DevOpsVPC/vpc.tf @@ -79,7 +79,7 @@ resource "aws_route_table" "private" { route { cidr_block = "0.0.0.0/0" - instance_id = "${aws_instance.NatInstance.id}" + instance_id = "${module.nat.instance_id}" # nat_gateway_id = "${element(split(",", var.nat_gateway_ids), count.index)}" } diff --git a/README.md b/README.md index 034ece5..6e22b8f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ This project uses Terraform to accomplish this goal. - [Security related](#security-related) - [Instances related](#instances-related) - [Other variables](#other-variables) +- [Modules](#modules) + - [NAT](#nat----modulesnat) - [Usage](#usage) - [Inspect the infrastructure](#make-plan) - [Apply changes](#make-apply) @@ -79,9 +81,7 @@ Project's data that can vary from one environment to another was exposed using v This file is automatically loaded when invoking terraform by the `Makefile wrapper`.
Refer to the `variables.tf` file in the `DevOpsVPC` directory for the default values. -[//]: # (Segment attributes by environment if multiple environments are created) - -[//]: # (Copy section for multiple separated environments.) +[//]: # (Segment attributes by category.) [//]: # (### Top Level Variables) [//]: # (|Variable |Type |Description |Comments |) @@ -103,10 +103,12 @@ Refer to the `variables.tf` file in the `DevOpsVPC` directory for the default va | `vpc_public_subnets` | *List* | List of `CIDR blocks` for our public subnets.
Must be in range of the `vpc_cidr`. | Mandatory | | `enable_dns_support` | *Boolean* | Should be `true` if you want to use private DNS within the VPC. | Optional, defaults to `true` | | `enable_dns_hostnames` | *Boolean* | Should be true if you want to use private DNS within the VPC. | Optional, defaults to `true` | +| `nat_inbound_ports` | *String* | Comma separated list of ports that will be opened on the public facing IP of the NAT instance.
Eg. SSH+VPN ports | Optional, defaults to `22,443` | ### Security related |Variable |Type |Description |Comments | |:---------|:----|:-----------|:--------| +| `ssh_user` | *String* | Name of the SSH used used to connect with during instance provisioning | Mandatory | | `ssh_public_key_name` | *String* | Name of the `SSH Key` that will be uploaded into AWS and used for SSH into instances. | Mandatory | | `ssh_public_key_file` | *String* | Location for the public ssh key file on your local workstation. | Mandatory | @@ -121,6 +123,36 @@ Refer to the `variables.tf` file in the `DevOpsVPC` directory for the default va |:---------|:----|:-----------|:--------| | `default_tags` | *Map* | A map of tags to add to all resources for audit, identification, etc. | Optional | +--- +[//]: # (Terraform Modules that this project provides.) + +[//]: # (### Exposed variables to configure the module.) +[//]: # (|Variable |Type |Description |Comments |) +[//]: # (|:---------|:----|:-----------|:--------|) +[//]: # (| | | | |) + +## Modules +### `NAT` -- `modules/nat/` +- Terraform module used to configure a EC2 instance that will serve as NAT Gateway/purpose for the instances that reside in the private subnet. +### Exposed variables: +|Variable |Type |Description |Comments | +|:---------|:----|:-----------|:--------| +| `instance_name` | *String* | Name of the Nat instance that will appear in AWS Console | Mandatory | +| `instance_type` | *String* | Type of the instance used that will serve as NAT Purpose | Optional | +| `vpc_name` | *String* | VPC name that the created instance will be assigned to. | Mandatory | +| `vpc_id` | *String* | VPC ID that the instance will be assigned to. | Mandatory | +| `subnet_id` | *String* | Subnet ID that will be `used for instance interface` creation. Eg. Public Subnet ID. | Mandatory | +| `private_subnets_cidr` | *String* | `CIDR of the private subnet` that the instance will do `NAT translation` for | Mandatory | +| `ami_id` | *String* | `AWS AMI ID` used for instance creation | Mandatory | +| `user_data` | *String* | `user_data` config used during instance creation | Mandatory | +| `sgs` | *String* | `Security groups IDs` that will be assigned to the NAT instance | Mandatory | +| `key_name` | *String* | `AWS Name of the ssh key` to be used during instance provisioning | Mandatory | +| `private_key_file` | *String* | Location for the private ssh key file that will be used to connect to the instance during provisioning | Mandatory | +| `number_of_instances` | *Integer* | Number of NAT instances to spawn | Optional, defaults to `1` | +| `root_volume_size` | *Integer* | Size in GBytes for the NAT instance root volume | Optional, defaults to `8` | +| `inbound_ports` | *String* | Comma separated list of ports that will be opened on the public facing IP of the NAT instance. Eg. SSH+VPN ports | Optional | + + --- ## **Usage** diff --git a/modules/nat/eip.tf b/modules/nat/eip.tf new file mode 100644 index 0000000..07a74f4 --- /dev/null +++ b/modules/nat/eip.tf @@ -0,0 +1,22 @@ +# +# Project Name:: en_infra_aws +# Module:: NAT +# +# Copyright (C) 2017 - Present +# Author: 'Mihai Vultur ' +# +# All rights reserved +# +# Description: +# Configures Elastic IP that will be associated with the NAT instance +# + +resource "aws_eip" "NatInstance" { + instance = "${aws_instance.NatInstance.id}" + vpc = true +} +#-- +resource "aws_eip_association" "eip_assoc_NatInstance" { + instance_id = "${aws_instance.NatInstance.id}" + allocation_id = "${aws_eip.NatInstance.id}" +} diff --git a/modules/nat/main.tf b/modules/nat/main.tf new file mode 100644 index 0000000..cc825ec --- /dev/null +++ b/modules/nat/main.tf @@ -0,0 +1,36 @@ +# +# Project Name:: en_infra_aws +# Module:: NAT +# +# Copyright (C) 2017 - Present +# Author: 'Mihai Vultur ' +# +# All rights reserved +# +# Description: +# Creation of the EC2 instance that will serve as NAT Gateway +# + +resource "aws_instance" "NatInstance" { + count = "${var.number_of_instances}" + ami = "${var.ami_id}" + instance_type = "${var.instance_type}" + subnet_id = "${element(split(",", var.subnet_id), count.index)}" + key_name = "${var.key_name}" + + vpc_security_group_ids = [ + "${split(",", var.sgs)}" + ] + + source_dest_check = false + + root_block_device { + volume_type = "gp2" + volume_size = "${var.root_volume_size}" + delete_on_termination = true + } + + tags = "${merge(map("ManageRunningTime", "WorkingHoursStop"), map("Provisioner", "terraform"), map("VPC", var.vpc_name), map("Name", format("%s_%s", var.vpc_name, var.instance_name)))}" + +} + diff --git a/modules/nat/output.tf b/modules/nat/output.tf new file mode 100644 index 0000000..d5b677d --- /dev/null +++ b/modules/nat/output.tf @@ -0,0 +1,30 @@ +# +# Project Name:: en_infra_aws +# Module:: NAT +# +# Copyright (C) 2017 - Present +# Author: 'Mihai Vultur ' +# +# All rights reserved +# +# Description: +# Output the parameters returned by NAT instance +# handy to be reused by other resources. +# + +output "instance_id" { + value = "${aws_instance.NatInstance.id}" +} + +output "private_ip" { + value = "${aws_instance.NatInstance.private_ip}" +} + +output "public_ip" { + value = "${aws_eip.NatInstance.public_ip}" +} + +output "instance_zone" { + value = "${aws_instance.NatInstance.availability_zone}" +} + diff --git a/modules/nat/remote-exec.tf b/modules/nat/remote-exec.tf new file mode 100644 index 0000000..23470ac --- /dev/null +++ b/modules/nat/remote-exec.tf @@ -0,0 +1,38 @@ +# +# Project Name:: en_infra_aws +# Module:: NAT +# +# Copyright (C) 2017 - Present +# Author: 'Mihai Vultur ' +# +# All rights reserved +# +# Description: +# Execute the required configuration for the EC2 instance to +# convert it into a fully functional NAT instance +# + +#-- workarround adding a triggered resource +#-- because of a circular dependency between +#-- eip and aws_instance +resource "null_resource" "preparation" { + triggers { + association_ip_address = "${aws_eip_association.eip_assoc_NatInstance.id}" + } + provisioner "remote-exec" { + inline = [ + "sudo iptables -t nat -C POSTROUTING -o eth0 -s ${var.private_subnets_cidr} -j MASQUERADE 2> /dev/null || sudo iptables -t nat -A POSTROUTING -o eth0 -s ${var.private_subnets_cidr} -j MASQUERADE", + "echo 'net.ipv4.ip_forward=1' | sudo tee -a /etc/sysctl.conf", + "echo 'net.ipv4.conf.eth0.send_redirects=0' | sudo tee -a /etc/sysctl.conf", + "echo 'net.netfilter.nf_conntrack_max=131072' | sudo tee -a /etc/sysctl.conf", + "sudo sysctl -p" + ] + connection { + host = "${aws_eip.NatInstance.public_ip}" + user = "centos" + timeout = "30s" + private_key = "${file(var.private_key_file)}" + } + } +} + diff --git a/modules/nat/security.tf b/modules/nat/security.tf new file mode 100644 index 0000000..e0969fd --- /dev/null +++ b/modules/nat/security.tf @@ -0,0 +1,31 @@ +# +# Project Name:: en_infra_aws +# Module:: NAT +# +# Copyright (C) 2017 - Present +# Author: 'Mihai Vultur ' +# +# All rights reserved +# +# Description: +# Provide the possibility to open some ports on the +# instance public facing IP. Usefull for ssh (jump host) or VPN. +# + +resource "aws_security_group" "DefaultNAT" { + vpc_id = "${var.vpc_id}" + description = "Default VPC security group for NAT instance" + + tags = "${merge(map("Provisioner", "terraform"), map("VPC", var.vpc_name), map("Name", format("%s_SC_DefaultNAT", var.vpc_name)))}" +} +#-- +resource "aws_security_group_rule" "NATRule" { + count = "${length(split(",",var.inbound_ports))}" + type = "ingress" + from_port = "${element(split(",", var.inbound_ports), count.index)}" + to_port = "${element(split(",", var.inbound_ports), count.index)}" + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + + security_group_id = "${aws_security_group.DefaultNAT.id}" +} diff --git a/modules/nat/variables.tf b/modules/nat/variables.tf new file mode 100644 index 0000000..b95abb0 --- /dev/null +++ b/modules/nat/variables.tf @@ -0,0 +1,62 @@ +# +# Project Name:: en_infra_aws +# Module:: NAT +# +# Copyright (C) 2017 - Present +# Author: 'Mihai Vultur ' +# +# All rights reserved +# +# Description: +# Module specific variables +# + +variable "instance_name" { + description = "Name of the Nat instance that will appear in AWS Console" +} +variable "instance_type" { + description = "Type of the instance used that will serve as NAT Purpose" + default = "t2.micro" +} +variable "vpc_name" { + description = "VPC name that the created instance will be assigned to" +} +variable "vpc_id" { + description = "VPC ID that the instance will be assigned to" +} +variable "subnet_id" { + description = "Subnet ID that will be used for instance interface creation. Eg. Public Subnet ID." +} +variable "private_subnets_cidr" { + description = "CIDR of the private subnet that the instance will do NAT translation for" +} +variable "ami_id" { + description = "AWS AMI ID used for instance creation" +} +variable "user_data" { + description = "user_data config used during instance creation" +} +variable "sgs" { + description = "Security groups IDs that will be assigned to the NAT instance" +} +variable "key_name" { + description = "AWS Name of the ssh key to be used during instance provisioning" +} +variable "private_key_file" { + description = "Path to private key file use to connect to the NAT Instance" +} + +variable "number_of_instances" { + description = "Number of instances to start" + default = 1 +} + +variable "root_volume_size" { + description = "Size of the root volume" + default = 8 +} + +variable "inbound_ports" { + description = "Comma separated list of ports that will be opened on the public facing IP of the NAT instance" + default = "22,443" +}