diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6c83f5d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,25 @@ +The MIT License (MIT) +===================== + +Copyright © 2024 Peter Sin, Vladimir "allejo" Jimenez + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..54cc6aa --- /dev/null +++ b/README.md @@ -0,0 +1,579 @@ +# Virtual Private Cloud Module + +This module creates and configures a [VPC](https://aws.amazon.com/vpc/) and multiple subnets, route tables, and gateways + +**This repository is a READ-ONLY sub-tree split**. See https://github.com/FriendsOfTerraform/modules to create issues or submit pull requests. + +## Table of Contents + +- [Example Usage](#example-usage) + - [Basic Usage](#basic-usage) + - [Flow Logs](#flow-logs) + - [Peering Connection Requests](#peering-connection-requests) +- [Argument Reference](#argument-reference) + - [Mandatory](#mandatory) + - [Optional](#optional) +- [Outputs](#outputs) +- [Known Limitations](#known-limitations) + - [vpc_endpoint_id conflicts with destination_prefix_list_id](#vpc_endpoint_id-conflicts-with-destination_prefix_list_id) + +## Example Usage + +### Basic Usage + +```terraform +module "basic_usage" { + source = "github.com/FriendsOfTerraform/aws-vpc.git?ref=v1.0.0" + + name = "demo-vpc" + + # When create_nat_gateways = true, one NAT gateway will be created on each public subnets' availability zone + # You can reference the gateway in the route table using "default-nat-gateway/" + # See below for an example + create_nat_gateways = true + + cidr_block = { + ipv4 = { + cidr = "10.0.4.0/22" + } + } + + subnets = { + # The key of the map will be the subnet's name + # A subnet is considered public if enable_auto_assign_public_ipv4_address = true + # An internet gateway will be created if at least one subnet is public + public-us-west-1a = { + ipv4_cidr_block = "10.0.4.0/24" + availability_zone = "us-west-1a" + enable_auto_assign_public_ipv4_address = true + } + public-us-west-1b = { + ipv4_cidr_block = "10.0.5.0/24" + availability_zone = "us-west-1b" + enable_auto_assign_public_ipv4_address = true + } + private-us-west-1a = { + ipv4_cidr_block = "10.0.6.0/24" + availability_zone = "us-west-1a" + } + private-us-west-1b = { + ipv4_cidr_block = "10.0.7.0/24" + availability_zone = "us-west-1b" + } + } + + route_tables = { + # The key of the map will be the route table's name + "public-route-table" = { + routes = { + # The key of the route will be the destination + # The value of the route will be the target + # default-internet-gateway refers to the internet gateway this module creates + "0.0.0.0/0" = "default-internet-gateway" + "10.0.10.0/16 = "tgw-012345a9e9dabcdef" + } + subnet_associations = ["public-us-west-1a", "public-us-west-1b"] + }, + "private-us-west-1a-route-table" = { + routes = { + # default-nat-gateway/us-west-1a refers to the NAT gateway in the us-west-1a availability zone + "0.0.0.0/0" = "default-nat-gateway/us-west-1a" + } + subnet_associations = ["private-us-west-1a"] + }, + "private-us-west-1b-route-table" = { + routes = { + "0.0.0.0/0" = "default-nat-gateway/us-west-1b" + } + subnet_associations = ["private-us-west-1b"] + } + } +} +``` + +### Flow Logs + +You can create flow logs at the VPC or the subnet level + +```terraform +module "flow_logs" { + source = "github.com/FriendsOfTerraform/aws-vpc.git?ref=v1.0.0" + + name = "demo-vpc" + + cidr_block = { + ipv4 = { + cidr = "10.0.4.0/22" + } + } + + subnets = { + public-us-west-1a = { + ipv4_cidr_block = "10.0.4.0/24" + availability_zone = "us-west-1a" + enable_auto_assign_public_ipv4_address = true + } + private-us-west-1a = { + ipv4_cidr_block = "10.0.6.0/24" + availability_zone = "us-west-1a" + + # Manages multiple subnet level flow logs + flow_logs = { + # The key of the map will be the flow log's name + subnet-flow-log = { + destination = { + cloudwatch_logs = { + log_group_arn = "arn:aws:logs:us-west-1:111122223333:log-group:demo-flow-logs-log-group" + } + } + } + } + } + } + + # Manages multiple VPC level flow logs + flow_logs = { + # The key of the map will be the flow log's name + vpc-flow-log = { + destination = { + s3 = { + bucket_arn = "arn:aws:s3:::demo-flow-logs-bucket" + } + } + } + } +} +``` + +### Peering Connection Requests + +```terraform +module "peering_connection_requests" { + source = "github.com/FriendsOfTerraform/aws-vpc.git?ref=v1.0.0" + + name = "demo-vpc" + + cidr_block = { + ipv4 = { + cidr = "10.0.4.0/22" + } + } + + peering_connection_requests = { + # The key of the map will be the peering connection name + # For peering connection requests at the same account and region, the connection will be automatically accepted + "peering-same-acccount-and-region" = { + peer_vpc_id = "vpc-0123450af84abcdef" + } + + # For peering connection requests at different account and/or region, a separate aws_vpc_peering_connection_accepter resource + # must be created at the target account/region to manage the accepter's side of the connection + "peering-same-account-different-region" = { + peer_vpc_id = "vpc-987654abcdabcdef" + peer_region = "us-east-1" + } + } + + route_tables = { + "private-route-table" = { + routes = { + # Peering connections made with this module can be referenced by name as a route target + "10.0.0.0/16" = "peering-same-acccount-and-region" + "172.25.100.0/24" = "peering-same-account-different-region" + } + } + } +} +``` + +## Argument Reference + +### Mandatory + +- (object) **`cidr_block`** _[since v1.0.0]_ + + Configures the VPC CIDR block + + - (object) **`ipv4 = null`** _[since v1.0.0]_ + + Configures the IPv4 CIDR block + + - (string) **`cidr = null`** _[since v1.0.0]_ + + Manually input an IPv4 CIDR. The CIDR block size must have a size between /16 and /28. Mutually exclusive to `ipam` + + - (object) **`ipam = null`** _[since v1.0.0]_ + + Specify an [Amazon VPC IP Address Manager (IPAM)][vpc-ipam] pool to obtain an IPv4 CIDR automatically. If you select an IPAM pool, the size of the CIDR is limited by the allocation rules on the IPAM pool (allowed minimum, allowed maximum, and default). Mutally exclusive to `cidr` + + - (string) **`pool_id`** _[since v1.0.0]_ + + The ID of an IPv4 IPAM pool you want to use for allocating this VPC's CIDR + + - (string) **`netmask`** _[since v1.0.0]_ + + The netmask length of the IPv4 CIDR you want to allocate to this VPC + +- (string) **`name`** _[since v1.0.0]_ + + The name of the VPC. All associated resources will also have their name prefixed with this value + +### Optional + +- (map(string)) **`additional_tags = {}`** _[since v1.0.0]_ + + Additional tags for the VPC + +- (map(string)) **`additional_tags_all = {}`** _[since v1.0.0]_ + + Additional tags for all resources deployed with this module + +- (bool) **`create_nat_gateways = false`** _[since v1.0.0]_ + + If enabled, one NAT gateway will be created on the first public subnets in each availability zone. You can then refer to them on the route table with `default-nat-gateway/`. Please see [example](#basic-usage) + +- (object) **`dhcp_options = null`** _[since v1.0.0]_ + + DHCP option sets give you control over various aspects of routing in your virtual network, such as the DNS servers, domain names, or Network Time Protocol (NTP) servers used by the devices in your VPC. The Amazon default option set will be used if not specified + + - (map(string)) **`additional_tags = {}`** _[since v1.0.0]_ + + Additional tags for the option set + + - (string) **`domain_name = null`** _[since v1.0.0]_ + + If you're using `AmazonProvidedDNS` in `us-east-1`, specify `ec2.internal`. If you're using `AmazonProvidedDNS` in another region, specify `region.compute.internal` (for example, `ap-northeast-1.compute.internal`). Otherwise, specify a domain name (for example, example.com). This value is used to complete unqualified DNS hostnames. + + - (list(string)) **`domain_name_servers = ["AmazonProvidedDNS"]`** _[since v1.0.0]_ + + The IP addresses of up to four domain name servers, or AmazonProvidedDNS. Although you can specify up to four domain name servers, note that some operating systems may impose lower limits. If you want your instance to receive a custom DNS hostname as specified in domain-name, you must set domain-name-servers to a custom DNS server. + + - (list(string)) **`ntp_servers = null`** _[since v1.0.0]_ + + The IP addresses of up to four NTP servers + + - (list(string)) **`netbios_name_servers = null`** _[since v1.0.0]_ + + The IP addresses of up to four NetBIOS name servers + + - (number) **`netbios_node_type = null`** _[since v1.0.0]_ + + The NetBIOS node type (`1`, `2`, `4`, or `8`). AWS recommends to specify `2` since broadcast and multicast are not supported in their network. + +- (object) **`dns_settings = {}`** _[since v1.0.0]_ + + Configures DNS settings for the VPC + + - (bool) **`enable_dns_resolution = true`** _[since v1.0.0]_ + + Whether DNS resolution through the Amazon DNS server is supported for the VPC + + - (bool) **`enable_dns_hostnames = false`** _[since v1.0.0]_ + + Whether instances launched in the VPC receive public DNS hostnames that correspond to their public IP addresses + +- (bool) **`enable_network_address_usage_metrics = false`** _[since v1.0.0]_ + + [Network Address Usage (NAU)][vpc-network-address-usage] is a metric applied to resources in your virtual network to help you plan for and monitor the size of your VPC + +- (map(object)) **`flow_logs = {}`** _[since v1.0.0]_ + + Configures multiple VPC level flow logs. Please see [example](#flow-logs) + + - (object) **`destination`** _[since v1.0.0]_ + + Where the flow log will be sent to. Must specify only one of the following: `cloudwatch_logs`, `s3` + + - (object) **`cloudwatch_logs = null`** _[since v1.0.0]_ + + Configures CloudWatch Logs as destination + + - (string) **`log_group_arn`** _[since v1.0.0]_ + + The ARN of the CloudWatch log group to send logs to + + - (string) **`service_role_arn = null`** _[since v1.0.0]_ + + Arn of an IAM role that [gives permission to flow logs to send logs to CloudWatch][vpc-flow-logs-cloudwatch-service-role]. A default service role will be created if not specified + + - (object) **`s3 = null`** _[since v1.0.0]_ + + Configures S3 as destination + + - (string) **`bucket_arn`** _[since v1.0.0]_ + + The ARN of the S3 bucket to send logs to + + - (string) **`log_file_format = "plain-text"`** _[since v1.0.0]_ + + The format for the flow log. Valid values: `"plain-text"`, `"parquet"` + + - (bool) **`enable_hive_compatible_s3_prefix = false`** _[since v1.0.0]_ + + Indicates whether to use Hive-compatible prefixes for flow logs stored in Amazon S3 + + - (bool) **`partition_logs_every_hour = false`** _[since v1.0.0]_ + + Indicates whether to partition the flow log per hour. This reduces the cost and response time for queries. + + - (map(string)) **`additional_tags = {}`** _[since v1.0.0]_ + + Additional tags for the flow log + + - (string) **`custom_log_record_format = null`** _[since v1.0.0]_ + + The fields to include in the flow log record. Accepted format example: `"$${interface-id} $${srcaddr} $${dstaddr} $${srcport} $${dstport}"`. Please refer to [this documentation][vpc-flow-logs-log-record-available-fields] for a list of available fields + + - (string) **`filter = "ALL"`** _[since v1.0.0]_ + + The type of traffic to capture. Valid values: `"ALL"`, `"ACCEPT"`, `"REJECT"` + + - (number) **`maximum_aggregation_interval = 600`** _[since v1.0.0]_ + + The maximum interval of time during which a flow of packets is captured and aggregated into a flow log record. Valid Values: `60 `seconds (1 minute) or `600` seconds (10 minutes). + +- (map(object)) **`peering_connection_requests = {}`** _[since v1.0.0]_ + + Manages multiple VPC peering connection requests. Please see [example](#peering-connection-requests) + + - (string) **`peer_vpc_id`** _[since v1.0.0]_ + + The ID of the target VPC with which you are creating the VPC Peering Connection + + - (map(string)) **`additional_tags = {}`** _[since v1.0.0]_ + + Additional tags for the peering connection request + + - (bool) **`allow_remote_vpc_dns_resolution = false`** _[since v1.0.0]_ + + Allow a local VPC to resolve public DNS hostnames to private IP addresses when queried from instances in the peer VPC. To use DNS resolution over peering you must enable DNS Hostname on both the requester's and accepter's VPC + + - (string) **`peer_account_id = null`** _[since v1.0.0]_ + + The AWS account ID of the target peer VPC. Defaults to the current account if unspecified. + + - (string) **`peer_region = null`** _[since v1.0.0]_ + + The region of the accepter VPC of the VPC Peering Connection. Defaults to the current region if unspecified. + +- (map(object)) **`route_tables = {}`** _[since v1.0.0]_ + + Manages multiple route tables. Please see [example](#basic-usage) + + - (map(string)) **`additional_tags = {}`** _[since v1.0.0]_ + + Additional tags for the route table + + - (map(string)) **`routes = {}`** _[since v1.0.0]_ + + Map of routes in the `{ = }` format + + - (list(string)) **`subnet_associations = []`** _[since v1.0.0]_ + + List of subnet names this route table is associated to + +- (map(object)) **`subnets = {}`** _[since v1.0.0]_ + + Manages multiple subnets. [See example](#basic-usage) + + - (string) **`availability_zone`** _[since v1.0.0]_ + + Availability zone of the subnet + + - (string) **`ipv4_cidr_block`** _[since v1.0.0]_ + + The IPv4 CIDR block for the subnet + + - (map(string)) **`additional_tags = {}`** _[since v1.0.0]_ + + Additional tags for the subnet + + - (bool) **`enable_auto_assign_public_ipv4_address = false`** _[since v1.0.0]_ + + If true, instances launched into the subnet should be assigned a public IP address. The subnet will also be considered a public subnet and an internet gateway will be created for the VPC. + + - (map(object)) **`flow_logs = {}`** _[since v1.0.0]_ + + Configures multiple subnet level flow logs. Please see [example](#flow-logs) + + - (object) **`destination`** _[since v1.0.0]_ + + Where the flow log will be sent to. Must specify only one of the following: `cloudwatch_logs`, `s3` + + - (object) **`cloudwatch_logs = null`** _[since v1.0.0]_ + + Configures CloudWatch Logs as destination + + - (string) **`log_group_arn`** _[since v1.0.0]_ + + The ARN of the CloudWatch log group to send logs to + + - (string) **`service_role_arn = null`** _[since v1.0.0]_ + + Arn of an IAM role that [gives permission to flow logs to send logs to CloudWatch][vpc-flow-logs-cloudwatch-service-role]. A default service role will be created if not specified + + - (object) **`s3 = null`** _[since v1.0.0]_ + + Configures S3 as destination + + - (string) **`bucket_arn`** _[since v1.0.0]_ + + The ARN of the S3 bucket to send logs to + + - (string) **`log_file_format = "plain-text"`** _[since v1.0.0]_ + + The format for the flow log. Valid values: `"plain-text"`, `"parquet"` + + - (bool) **`enable_hive_compatible_s3_prefix = false`** _[since v1.0.0]_ + + Indicates whether to use Hive-compatible prefixes for flow logs stored in Amazon S3 + + - (bool) **`partition_logs_every_hour = false`** _[since v1.0.0]_ + + Indicates whether to partition the flow log per hour. This reduces the cost and response time for queries. + + - (map(string)) **`additional_tags = {}`** _[since v1.0.0]_ + + Additional tags for the flow log + + - (string) **`custom_log_record_format = null`** _[since v1.0.0]_ + + The fields to include in the flow log record. Accepted format example: `"$${interface-id} $${srcaddr} $${dstaddr} $${srcport} $${dstport}"`. Please refer to [this documentation][vpc-flow-logs-log-record-available-fields] for a list of available fields + + - (string) **`filter = "ALL"`** _[since v1.0.0]_ + + The type of traffic to capture. Valid values: `"ALL"`, `"ACCEPT"`, `"REJECT"` + + - (number) **`maximum_aggregation_interval = 600`** _[since v1.0.0]_ + + The maximum interval of time during which a flow of packets is captured and aggregated into a flow log record. Valid Values: `60 `seconds (1 minute) or `600` seconds (10 minutes). + + - (object) **`resource_based_name_settings = {}`** _[since v1.0.0]_ + + Specify the hostname type for EC2 instances in this subnet and optional RBN DNS query settings + + - (bool) **`enable_resource_name_dns_a_record_on_launch = false`** _[since v1.0.0]_ + + Choose if DNS A record queries for the resource-based name should return the IPv4 address or not + + - (string) **`hostname_type = "ip-name"`** _[since v1.0.0]_ + + Determines if the guest OS hostname of EC2 instances in this subnet should be based on the resource name (RBN) or the IP name (IPBN). Valid values: `"ip-name"`, `"resource-name"`. If you choose `"resource-name"`, when you launch an EC2 instance in this subnet, the guest OS hostname of the EC2 instance will be configured to use the EC2 instance ID: `ec2-instance-id.region.compute.internal`. If you choose `"ip-name"`, when you launch an EC2 instance in this subnet, the guest OS hostname of the EC2 instance will be configured to use an IP-based name: `private-ipv4-address.region.compute.internal` + +- (string) **`tenancy = "default"`** _[since v1.0.0]_ + + Specify the VPC's tenancy. Valid values: `"default"`, `"dedicated"` + +## Outputs + +- (object) **`dhcp_options`** _[since v1.0.0]_ + + DHCP option + + - (string) **`arn`** _[since v1.0.0]_ + + The ARN of the DHCP option + + - (string) **`id`** _[since v1.0.0]_ + + The ID of the DHCP option + +- (object) **`internet_gateway`** _[since v1.0.0]_ + + The default internet gateway + + - (string) **`arn`** _[since v1.0.0]_ + + The ARN of the internet gateway + + - (string) **`id`** _[since v1.0.0]_ + + The ID of the internet gateway + + - (string) **`owner_id`** _[since v1.0.0]_ + + The ID of the AWS account that owns the internet gateway + +- (map(object)) **`nat_gateways`** _[since v1.0.0]_ + + Default NAT gateways. The key of the map is the NAT gateway's name + + - (string) **`availability_zone`** _[since v1.0.0]_ + + The availability of the NAT gateway + + - (string) **`association_id`** _[since v1.0.0]_ + + The association ID of the Elastic IP address that's associated with the NAT Gateway + + - (string) **`id`** _[since v1.0.0]_ + + The ID of the NAT gateway + + - (string) **`network_interface_id`** _[since v1.0.0]_ + + The ID of the network interface associated with the NAT Gateway + + - (string) **`public_ip`** _[since v1.0.0]_ + + The Elastic IP address associated with the NAT Gateway + +- (map(object)) **`peering_connection_requests`** _[since v1.0.0]_ + + Map of peering connection requests. The key of the map is the peering connection request's name + + - (string) **`id`** _[since v1.0.0]_ + + The peering connection ID + + - (string) **`accept_status`** _[since v1.0.0]_ + + The status of the VPC Peering Connection request + +- (map(object)) **`route_tables`** _[since v1.0.0]_ + + Map of route tables. The key of the map is the route table's name + + - (string) **`arn`** _[since v1.0.0]_ + + The ARN of the route tables + + - (string) **`id`** _[since v1.0.0]_ + + The ID of the route tables + +- (map(object)) **`subnets`** _[since v1.0.0]_ + + Map of subnets. The key of the map is the subnet's name + + - (string) **`arn`** _[since v1.0.0]_ + + The ARN of the subnets + + - (string) **`id`** _[since v1.0.0]_ + + The ID of the subnets + + - (string) **`owner_id`** _[since v1.0.0]_ + + The ID of the AWS account that owns the subnet + +- (string) **`vpc_arn`** _[since v1.0.0]_ + + The ARN of the VPC + +- (string) **`vpc_id`** _[since v1.0.0]_ + + The ID of the VPC + +## Known Limitations + +### vpc_endpoint_id conflicts with destination_prefix_list_id + +Specifying a route with a prefix_list_id as destination and vpc_endpoint_id as target, for example: `{ "pl-02cabcde" = "vpce-0123454fcbbabcdef" }` will return an error `vpc_endpoint_id conflicts with destination_prefix_list_id`. This is expected since the AWS API disallow this combination. VPC endpoints must be associated with the route table separately using the [aws_vpc_endpoint_route_table_association][terraform-aws-provider-aws_vpc_endpoint_route_table_association] instead. + +[terraform-aws-provider-aws_vpc_endpoint_route_table_association]:https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_endpoint_route_table_association +[vpc-flow-logs-cloudwatch-service-role]:https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs-iam-role.html +[vpc-flow-logs-log-record-available-fields]:https://docs.aws.amazon.com/vpc/latest/userguide/flow-log-records.html#flow-logs-fields +[vpc-ipam]:https://docs.aws.amazon.com/vpc/latest/ipam/what-it-is-ipam.html +[vpc-network-address-usage]:https://docs.aws.amazon.com/vpc/latest/userguide/network-address-usage.html diff --git a/_common.tf b/_common.tf new file mode 100644 index 0000000..1c11376 --- /dev/null +++ b/_common.tf @@ -0,0 +1,10 @@ +locals { + common_tags = { + managed-by = "Terraform" + } + + private_subnets = [for k, v in var.subnets : k if !v.enable_auto_assign_public_ipv4_address] + private_subnet_ids_by_availability_zones = { for subnet in values(aws_subnet.subnets) : subnet.availability_zone => subnet.id... if !subnet.map_public_ip_on_launch } + public_subnets = [for k, v in var.subnets : k if v.enable_auto_assign_public_ipv4_address] + public_subnet_ids_by_availability_zones = { for subnet in values(aws_subnet.subnets) : subnet.availability_zone => subnet.id... if subnet.map_public_ip_on_launch } +} diff --git a/eip.tf b/eip.tf new file mode 100644 index 0000000..5f3493c --- /dev/null +++ b/eip.tf @@ -0,0 +1,10 @@ +resource "aws_eip" "nat_elastic_ips" { + for_each = var.create_nat_gateways ? local.public_subnet_ids_by_availability_zones : {} + + domain = "vpc" + + tags = merge( + { Name = "${var.name}-${each.key}-nat-gateway" }, + var.additional_tags_all + ) +} diff --git a/flow-log.tf b/flow-log.tf new file mode 100644 index 0000000..a41c29f --- /dev/null +++ b/flow-log.tf @@ -0,0 +1,69 @@ +locals { + subnet_flow_logs = flatten([ + for subnet_name, subnet_config in var.subnets : [ + for flow_log_name, flow_log_config in subnet_config.flow_logs : { + flow_log_name = flow_log_name + subnet_name = subnet_name + destination = flow_log_config.destination + additional_tags = flow_log_config.additional_tags + custom_log_record_format = flow_log_config.custom_log_record_format + filter = flow_log_config.filter + maximum_aggregation_interval = flow_log_config.maximum_aggregation_interval + } + ] + ]) +} + +resource "aws_flow_log" "vpc_flow_logs" { + for_each = var.flow_logs + + traffic_type = each.value.filter + iam_role_arn = each.value.destination.cloudwatch_logs != null ? (each.value.destination.cloudwatch_logs.service_role_arn != null ? each.value.destination.cloudwatch_logs.service_role_arn : aws_iam_role.flow_logs_cloudwatch_logs_service_role[0].arn) : null + log_destination_type = each.value.destination.cloudwatch_logs != null ? "cloud-watch-logs" : (each.value.destination.s3 != null ? "s3" : null) + log_destination = each.value.destination.cloudwatch_logs != null ? each.value.destination.cloudwatch_logs.log_group_arn : (each.value.destination.s3 != null ? each.value.destination.s3.bucket_arn : null) + vpc_id = aws_vpc.vpc.id + max_aggregation_interval = each.value.maximum_aggregation_interval + + dynamic "destination_options" { + for_each = each.value.destination.s3 != null ? [1] : [] + + content { + file_format = each.value.destination.s3.log_file_format + hive_compatible_partitions = each.value.destination.s3.enable_hive_compatible_s3_prefix + per_hour_partition = each.value.destination.s3.partition_logs_every_hour + } + } + + tags = merge( + { Name = each.key }, + each.value.additional_tags, + var.additional_tags_all + ) +} + +resource "aws_flow_log" "subnet_flow_logs" { + for_each = tomap({ for flow_log in local.subnet_flow_logs : "${flow_log.subnet_name}-${flow_log.flow_log_name}" => flow_log }) + + traffic_type = each.value.filter + iam_role_arn = each.value.destination.cloudwatch_logs != null ? (each.value.destination.cloudwatch_logs.service_role_arn != null ? each.value.destination.cloudwatch_logs.service_role_arn : aws_iam_role.flow_logs_cloudwatch_logs_service_role[0].arn) : null + log_destination_type = each.value.destination.cloudwatch_logs != null ? "cloud-watch-logs" : (each.value.destination.s3 != null ? "s3" : null) + log_destination = each.value.destination.cloudwatch_logs != null ? each.value.destination.cloudwatch_logs.log_group_arn : (each.value.destination.s3 != null ? each.value.destination.s3.bucket_arn : null) + subnet_id = aws_subnet.subnets[each.value.subnet_name].id + max_aggregation_interval = each.value.maximum_aggregation_interval + + dynamic "destination_options" { + for_each = each.value.destination.s3 != null ? [1] : [] + + content { + file_format = each.value.destination.s3.log_file_format + hive_compatible_partitions = each.value.destination.s3.enable_hive_compatible_s3_prefix + per_hour_partition = each.value.destination.s3.partition_logs_every_hour + } + } + + tags = merge( + { Name = each.value.flow_log_name }, + each.value.additional_tags, + var.additional_tags_all + ) +} diff --git a/iam-policy.tf b/iam-policy.tf new file mode 100644 index 0000000..165159f --- /dev/null +++ b/iam-policy.tf @@ -0,0 +1,29 @@ +locals { + vpc_flow_logs_with_cloudwatch_logs_destination = { for k, v in var.flow_logs : k => v if v.destination.cloudwatch_logs != null } + vpc_flow_logs_with_cloudwatch_logs_destination_service_roles = { for k, v in local.vpc_flow_logs_with_cloudwatch_logs_destination : k => v if v.destination.cloudwatch_logs.service_role_arn == null } + subnet_flow_logs_with_cloudwatch_logs_destination = { for flow_log in local.subnet_flow_logs : "${flow_log.subnet_name}-${flow_log.flow_log_name}" => flow_log if flow_log.destination.cloudwatch_logs != null } + subnet_flow_logs_with_cloudwatch_logs_destination_service_roles = { for k, v in local.subnet_flow_logs_with_cloudwatch_logs_destination : k => v if v.destination.cloudwatch_logs.service_role_arn == null } +} + +resource "aws_iam_policy" "flow_logs_cloudwatch_logs_service_role" { + count = length(local.vpc_flow_logs_with_cloudwatch_logs_destination_service_roles) > 0 ? 1 : (length(local.subnet_flow_logs_with_cloudwatch_logs_destination_service_roles) > 0 ? 1 : 0) + + name = "vpc-flow-logs-${var.name}" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogGroups", + "logs:DescribeLogStreams" + ] + Resource = "*" + } + ] + }) +} diff --git a/iam-role.tf b/iam-role.tf new file mode 100644 index 0000000..edf6651 --- /dev/null +++ b/iam-role.tf @@ -0,0 +1,27 @@ +resource "aws_iam_role" "flow_logs_cloudwatch_logs_service_role" { + count = length(local.vpc_flow_logs_with_cloudwatch_logs_destination_service_roles) > 0 ? 1 : (length(local.subnet_flow_logs_with_cloudwatch_logs_destination_service_roles) > 0 ? 1 : 0) + + name = "vpc-flow-logs-${var.name}" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow", + Principal = { + Service = "vpc-flow-logs.amazonaws.com" + } + Action = [ + "sts:AssumeRole" + ] + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "flow_logs_cloudwatch_logs_service_role" { + count = length(local.vpc_flow_logs_with_cloudwatch_logs_destination_service_roles) > 0 ? 1 : (length(local.subnet_flow_logs_with_cloudwatch_logs_destination_service_roles) > 0 ? 1 : 0) + + role = aws_iam_role.flow_logs_cloudwatch_logs_service_role[0].name + policy_arn = aws_iam_policy.flow_logs_cloudwatch_logs_service_role[0].arn +} diff --git a/internet-gateway.tf b/internet-gateway.tf new file mode 100644 index 0000000..bebeb61 --- /dev/null +++ b/internet-gateway.tf @@ -0,0 +1,10 @@ +resource "aws_internet_gateway" "internet_gateway" { + count = length(local.public_subnets) > 0 ? 1 : 0 + + vpc_id = aws_vpc.vpc.id + + tags = merge( + { Name = "${var.name}-internet-gateway" }, + var.additional_tags_all + ) +} diff --git a/nat-gateway.tf b/nat-gateway.tf new file mode 100644 index 0000000..acf4924 --- /dev/null +++ b/nat-gateway.tf @@ -0,0 +1,12 @@ +resource "aws_nat_gateway" "nat_gateway" { + for_each = var.create_nat_gateways ? local.public_subnet_ids_by_availability_zones : {} + + allocation_id = aws_eip.nat_elastic_ips[each.key].allocation_id + connectivity_type = "public" + subnet_id = each.value[0] # one NAT gateway will be deployed in the first public subnet of each availability zone + + tags = merge( + { Name = "${var.name}-${each.key}-nat-gateway" }, + var.additional_tags_all + ) +} diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..d798d97 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,66 @@ +output "dhcp_options" { + value = var.dhcp_options != null ? { + id = aws_vpc_dhcp_options.dhcp_options[0].id + arn = aws_vpc_dhcp_options.dhcp_options[0].arn + } : {} +} + +output "internet_gateway" { + value = length(local.public_subnets) > 0 ? { + id = aws_internet_gateway.internet_gateway[0].id + arn = aws_internet_gateway.internet_gateway[0].arn + owner_id = aws_internet_gateway.internet_gateway[0].owner_id + } : {} +} + +output "nat_gateways" { + value = var.create_nat_gateways != null ? { + for k, v in aws_nat_gateway.nat_gateway : + v.tags.Name => { + availability_zone = k + association_id = v.association_id + id = v.id + network_interface_id = v.network_interface_id + public_ip = v.public_ip + } + } : {} +} + +output "peering_connection_requests" { + value = { + for k, v in aws_vpc_peering_connection.peering_connection_requests : + k => { + id = v.id + accept_status = v.accept_status + } + } +} + +output "route_tables" { + value = { + for k, v in aws_route_table.route_tables : + k => { + id = v.id + arn = v.arn + } + } +} + +output "subnets" { + value = { + for k, v in aws_subnet.subnets : + k => { + id = v.id + arn = v.arn + owner_id = v.owner_id + } + } +} + +output "vpc_arn" { + value = aws_vpc.vpc.arn +} + +output "vpc_id" { + value = aws_vpc.vpc.id +} diff --git a/route-table-association.tf b/route-table-association.tf new file mode 100644 index 0000000..7b864b4 --- /dev/null +++ b/route-table-association.tf @@ -0,0 +1,18 @@ +locals { + route_table_associations = flatten([ + for k, v in var.route_tables : [ + for association in v.subnet_associations : + { + route_table_name = k + subnet_name = association + } + ] + ]) +} + +resource "aws_route_table_association" "route_table_associations" { + for_each = toset([for k in local.route_table_associations : "${k.route_table_name}~${k.subnet_name}"]) + + subnet_id = aws_subnet.subnets[split("~", each.value)[1]].id + route_table_id = aws_route_table.route_tables[split("~", each.value)[0]].id +} diff --git a/route-table.tf b/route-table.tf new file mode 100644 index 0000000..80da626 --- /dev/null +++ b/route-table.tf @@ -0,0 +1,11 @@ +resource "aws_route_table" "route_tables" { + for_each = var.route_tables + + vpc_id = aws_vpc.vpc.id + + tags = merge( + { Name = each.key }, + each.value.additional_tags, + var.additional_tags_all + ) +} diff --git a/route.tf b/route.tf new file mode 100644 index 0000000..49415b6 --- /dev/null +++ b/route.tf @@ -0,0 +1,31 @@ +locals { + routes = flatten([ + for k, v in var.route_tables : [ + for route_dest, route_target in v.routes : { + route_table_name = k, + route_destination = route_dest + route_target = route_target + } + ] + ]) +} + +resource "aws_route" "routes" { + for_each = tomap({ for route in local.routes : "${route.route_table_name}~${route.route_destination}" => route }) + + route_table_id = aws_route_table.route_tables[each.value.route_table_name].id + destination_cidr_block = length(regexall("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(3[0-2]|[1-2][0-9]|[0-9]))$", each.value.route_destination)) > 0 ? each.value.route_destination : null + destination_ipv6_cidr_block = length(regexall("^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/(12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))$", each.value.route_destination)) > 0 ? each.value.route_destination : null + destination_prefix_list_id = length(regexall("pl-[0-9a-f]{8}", each.value.route_destination)) > 0 ? each.value.route_destination : null + + carrier_gateway_id = startswith(each.value.route_target, "cagw-") ? each.value.route_target : null + core_network_arn = startswith(each.value.route_target, "arn:") ? each.value.route_target : null + egress_only_gateway_id = startswith(each.value.route_target, "eigw-") ? each.value.route_target : null + gateway_id = each.value.route_target == "default-internet-gateway" ? aws_internet_gateway.internet_gateway[0].id : (startswith(each.value.route_target, "vgw-") ? each.value.route_target : null) + nat_gateway_id = startswith(each.value.route_target, "default-nat-gateway/") ? aws_nat_gateway.nat_gateway[split("/", each.value.route_target)[1]].id : (startswith(each.value.route_target, "nat-") ? each.value.route_target : null) + local_gateway_id = startswith(each.value.route_target, "lgw-") ? each.value.route_target : null + network_interface_id = startswith(each.value.route_target, "eni-") ? each.value.route_target : null + transit_gateway_id = startswith(each.value.route_target, "tgw-") ? each.value.route_target : null + vpc_endpoint_id = startswith(each.value.route_target, "vpce-") ? each.value.route_target : null + vpc_peering_connection_id = contains(keys(var.peering_connection_requests), each.value.route_target) ? aws_vpc_peering_connection.peering_connection_requests[each.value.route_target].id : (startswith(each.value.route_target, "pcx-") ? each.value.route_target : null) +} diff --git a/subnet.tf b/subnet.tf new file mode 100644 index 0000000..e707c40 --- /dev/null +++ b/subnet.tf @@ -0,0 +1,18 @@ +resource "aws_subnet" "subnets" { + for_each = var.subnets + + availability_zone = each.value.availability_zone + cidr_block = each.value.ipv4_cidr_block + enable_resource_name_dns_a_record_on_launch = each.value.resource_based_name_settings.enable_resource_name_dns_a_record_on_launch + map_public_ip_on_launch = each.value.enable_auto_assign_public_ipv4_address + private_dns_hostname_type_on_launch = each.value.resource_based_name_settings.hostname_type + vpc_id = aws_vpc.vpc.id + + tags = merge( + { + Name = each.key + }, + each.value.additional_tags, + var.additional_tags_all + ) +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..f90068c --- /dev/null +++ b/variables.tf @@ -0,0 +1,153 @@ +variable "cidr_block" { + type = object({ + ipv4 = object({ + cidr = optional(string, null) + ipam = optional(object({ + pool_id = string + netmask = string + }), null) + }) + }) + description = "Specify the VPC CIDR block" +} + +variable "name" { + type = string + description = "The name of the VPC. All associated resources' names will also be prefixed by this value" +} + +variable "additional_tags" { + type = map(string) + description = "Additional tags for the VPC" + default = {} +} + +variable "additional_tags_all" { + type = map(string) + description = "Additional tags for all resources deployed with this module" + default = {} +} + +variable "create_nat_gateways" { + type = bool + description = "Create default NAT gateways" + default = false +} + +variable "dhcp_options" { + type = object({ + domain_name = optional(string, null) + domain_name_servers = optional(list(string), ["AmazonProvidedDNS"]) + ntp_servers = optional(list(string), null) + netbios_name_servers = optional(list(string), null) + netbios_node_type = optional(number, null) + additional_tags = optional(map(string), {}) + }) + description = "Configure DHCP options" + default = null +} + +variable "dns_settings" { + type = object({ + enable_dns_resolution = optional(bool, true) + enable_dns_hostnames = optional(bool, false) + }) + description = "Configure DNS settings" + default = {} +} + +variable "enable_network_address_usage_metrics" { + type = bool + description = "Enable Network Address Usage meteric" + default = false +} + +variable "flow_logs" { + type = map(object({ + destination = object({ + cloudwatch_logs = optional(object({ + log_group_arn = string + service_role_arn = optional(string, null) + }), null) + + s3 = optional(object({ + bucket_arn = string + log_file_format = optional(string, "plain-text") + enable_hive_compatible_s3_prefix = optional(bool, false) + partition_logs_every_hour = optional(bool, false) + }), null) + }) + + additional_tags = optional(map(string), {}) + custom_log_record_format = optional(string, null) + filter = optional(string, "ALL") + maximum_aggregation_interval = optional(number, 600) + })) + description = "Configure multiple flow logs" + default = {} +} + +variable "peering_connection_requests" { + type = map(object({ + peer_vpc_id = string + additional_tags = optional(map(string), {}) + allow_remote_vpc_dns_resolution = optional(bool, false) + peer_account_id = optional(string, null) + peer_region = optional(string, null) + })) + description = "Manage peering connection requests" + default = {} +} + +variable "route_tables" { + type = map(object({ + additional_tags = optional(map(string), {}) + routes = optional(map(string), {}) + subnet_associations = optional(list(string), []) + })) + description = "Manage multiple route tables" + default = {} +} + +variable "subnets" { + type = map(object({ + availability_zone = string + ipv4_cidr_block = string + additional_tags = optional(map(string), {}) + enable_auto_assign_public_ipv4_address = optional(bool, false) + + flow_logs = optional(map(object({ + destination = object({ + cloudwatch_logs = optional(object({ + log_group_arn = string + service_role_arn = optional(string, null) + }), null) + + s3 = optional(object({ + bucket_arn = string + log_file_format = optional(string, "plain-text") + enable_hive_compatible_s3_prefix = optional(bool, false) + partition_logs_every_hour = optional(bool, false) + }), null) + }) + + additional_tags = optional(map(string), {}) + custom_log_record_format = optional(string, null) + filter = optional(string, "ALL") + maximum_aggregation_interval = optional(number, 600) + })), {}) + + resource_based_name_settings = optional(object({ + enable_resource_name_dns_a_record_on_launch = optional(bool, false) + hostname_type = optional(string, "ip-name") + }), {}) + })) + description = "Configure multiple subnets" + default = {} +} + +variable "tenancy" { + type = string + description = "Specify the VPC's tenancy" + default = "default" +} diff --git a/vpc-dhcp-options-association.tf b/vpc-dhcp-options-association.tf new file mode 100644 index 0000000..f46fbba --- /dev/null +++ b/vpc-dhcp-options-association.tf @@ -0,0 +1,6 @@ +resource "aws_vpc_dhcp_options_association" "dhcp_options_association" { + count = var.dhcp_options != null ? 1 : 0 + + vpc_id = aws_vpc.vpc.id + dhcp_options_id = aws_vpc_dhcp_options.dhcp_options[0].id +} diff --git a/vpc-dhcp-options.tf b/vpc-dhcp-options.tf new file mode 100644 index 0000000..1b9fd16 --- /dev/null +++ b/vpc-dhcp-options.tf @@ -0,0 +1,17 @@ +resource "aws_vpc_dhcp_options" "dhcp_options" { + count = var.dhcp_options != null ? 1 : 0 + + domain_name = var.dhcp_options.domain_name + domain_name_servers = var.dhcp_options.domain_name_servers + ntp_servers = var.dhcp_options.ntp_servers + netbios_name_servers = var.dhcp_options.netbios_name_servers + netbios_node_type = var.dhcp_options.netbios_node_type + + tags = merge( + { + Name = "${var.name}-dhcp-options" + }, + var.dhcp_options.additional_tags, + var.additional_tags_all + ) +} diff --git a/vpc-peering-connection.tf b/vpc-peering-connection.tf new file mode 100644 index 0000000..cff20d4 --- /dev/null +++ b/vpc-peering-connection.tf @@ -0,0 +1,47 @@ +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +locals { + peering_connection_requests = { + for k, v in var.peering_connection_requests : + k => { + peer_vpc_id = v.peer_vpc_id + additional_tags = v.additional_tags + allow_remote_vpc_dns_resolution = v.allow_remote_vpc_dns_resolution + peer_account_id = v.peer_account_id == null ? null : (v.peer_account_id == data.aws_caller_identity.current.account_id ? null : v.peer_account_id) + peer_region = v.peer_region == null ? null : (v.peer_region == data.aws_region.current.name ? null : v.peer_region) + } + } +} + +resource "aws_vpc_peering_connection" "peering_connection_requests" { + for_each = local.peering_connection_requests + + peer_owner_id = each.value.peer_account_id + peer_vpc_id = each.value.peer_vpc_id + vpc_id = aws_vpc.vpc.id + auto_accept = each.value.peer_account_id == null ? (each.value.peer_region == null ? true : false) : false + peer_region = each.value.peer_region + + dynamic "accepter" { + for_each = each.value.peer_account_id == null ? (each.value.peer_region == null ? [1] : []) : [] + + content { + allow_remote_vpc_dns_resolution = each.value.allow_remote_vpc_dns_resolution + } + } + + dynamic "requester" { + for_each = each.value.peer_account_id != null ? [1] : (each.value.peer_region != null ? [1] : []) + + content { + allow_remote_vpc_dns_resolution = each.value.allow_remote_vpc_dns_resolution + } + } + + tags = merge( + { Name = each.key }, + each.value.additional_tags, + var.additional_tags_all + ) +} diff --git a/vpc.tf b/vpc.tf new file mode 100644 index 0000000..3205bb6 --- /dev/null +++ b/vpc.tf @@ -0,0 +1,15 @@ +resource "aws_vpc" "vpc" { + cidr_block = var.cidr_block.ipv4.cidr + instance_tenancy = var.tenancy + ipv4_ipam_pool_id = var.cidr_block.ipv4.ipam != null ? var.cidr_block.ipv4.ipam.pool_id : null + ipv4_netmask_length = var.cidr_block.ipv4.ipam != null ? var.cidr_block.ipv4.ipam.netmask : null + enable_dns_support = var.dns_settings.enable_dns_resolution + enable_network_address_usage_metrics = var.enable_network_address_usage_metrics + enable_dns_hostnames = var.dns_settings.enable_dns_hostnames + + tags = merge( + { Name = var.name }, + var.additional_tags, + var.additional_tags_all + ) +}