Skip to content

microsoftexpert/tf-mod-aws-vpc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS VPC Terraform Module

Secure-by-default Amazon VPC foundation — VPC + public / private / database subnets, route tables, Internet Gateway, configurable NAT topology, DHCP options, VPC Flow Logs (ON by default), and a deny-all default security group, all from a single composite call. Built for the AWS provider v6.x.

Terraform aws module type resources


🧩 Overview

  • 🏗️ Provisions an aws_vpc keystone plus its full network fabric in one call — no hand-wiring a dozen resources.
  • 🌐 Three opinionated subnet tiers — public, private (application), and database (data) — each a map(object(...)) keyed by a stable name for clean for_each placement across AZs.
  • 🚪 Internet Gateway auto-created when public subnets exist; NAT gateways with Elastic IPs in single, per_az, or none topology — a documented cost/HA tradeoff.
  • 🧭 Per-tier route tables: public → IGW, private → NAT, database isolated (no default route) unless explicitly opted in.
  • 🔒 Default security group adopted and locked to deny-all — the secure invariant; workloads attach to purpose-built SGs from tf-mod-aws-security-group.
  • 📊 VPC Flow Logs enabled by default (ALL traffic) to a caller-supplied CloudWatch Log Group, S3 bucket, or Firehose stream — observability the regulated-industry posture expects.
  • 🏷️ Universal taggingvar.tags flows to every taggable resource; per-subnet and per-DHCP tags merge on top; tags_all surfaced as an output.
  • 🧱 Optional secondary IPv4 CIDRs, Amazon-provided IPv6 /56, and custom DHCP option sets.

💡 Why it matters: Nearly every networking, compute, and database module in the library wires into this one. A secure, routable, observable VPC delivered from a single module call means the blast radius of a misconfigured network — large in a regulated FI — is closed off at the foundation.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


🗺️ Where this fits in the family

tf-mod-aws-vpc is a foundation module: it consumes no sibling module and originates the vpc_id, subnet_ids, and route-table ids that the rest of the suite depends on. Its only optional inputs are by-reference Flow Log targets.

flowchart LR
 vpc["tf-mod-aws-vpc"]
 kms["tf-mod-aws-kms"]
 iam["tf-mod-aws-iam-role"]
 cwlg["tf-mod-aws-cloudwatch-log-group"]
 s3["tf-mod-aws-s3-bucket"]
 sg["tf-mod-aws-security-group"]
 nacl["tf-mod-aws-network-acl"]
 ep["tf-mod-aws-vpc-endpoint"]
 lb["tf-mod-aws-lb"]
 rds["tf-mod-aws-rds"]
 eks["tf-mod-aws-eks"]
 tgw["tf-mod-aws-transit-gateway"]

 kms -. "flow_log_kms_key_arn (on target)".-> vpc
 iam -. "flow_log_iam_role_arn".-> vpc
 cwlg -. "flow_log_destination_arn".-> vpc
 s3 -. "flow_log_destination_arn".-> vpc

 vpc -- "vpc_id" --> sg
 vpc -- "vpc_id" --> nacl
 vpc -- "vpc_id / route_table_ids" --> ep
 vpc -- "public_subnet_ids" --> lb
 vpc -- "database_subnet_ids" --> rds
 vpc -- "private_subnet_ids" --> eks
 vpc -- "route_table_ids" --> tgw

 style vpc fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
Loading

🧬 What this module builds

flowchart TB
 subgraph VPCMOD["tf-mod-aws-vpc"]
 vpc["aws_vpc.this<br/>(keystone)"]
 dsg["aws_default_security_group.this<br/>deny-all"]
 sec["aws_vpc_ipv4_cidr_block_association.secondary<br/>(secondary CIDRs)"]
 dhcp["aws_vpc_dhcp_options.this<br/>(+ aws_vpc_dhcp_options_association.this)"]
 fl["aws_flow_log.this<br/>(ON by default)"]

 subgraph PUB["Public tier"]
 psub["aws_subnet.public"]
 igw["aws_internet_gateway.this"]
 eip["aws_eip.nat"]
 nat["aws_nat_gateway.this"]
 prt["aws_route_table.public<br/>0.0.0.0/0 -> IGW"]
 rpi["aws_route.public_internet<br/>0.0.0.0/0 -> IGW"]
 rta_pub["aws_route_table_association.public"]
 end
 subgraph PRIV["Private tier"]
 vsub["aws_subnet.private"]
 vrt["aws_route_table.private<br/>0.0.0.0/0 -> NAT"]
 rpn["aws_route.private_nat<br/>0.0.0.0/0 -> NAT"]
 rta_priv["aws_route_table_association.private"]
 end
 subgraph DB["Database tier"]
 dsub["aws_subnet.database"]
 drt["aws_route_table.database<br/>isolated (no default route)"]
 rdn["aws_route.database_nat<br/>0.0.0.0/0 -> NAT (optional)"]
 rta_db["aws_route_table_association.database"]
 end

 vpc --> psub & vsub & dsub
 vpc --> dsg & sec & dhcp & fl & igw
 igw --> prt
 prt --> rpi
 psub --> eip --> nat
 nat --> vrt
 vrt --> rpn
 psub --> rta_pub
 prt --- rta_pub
 vsub --> rta_priv
 vrt --- rta_priv
 dsub --> rta_db
 drt --- rta_db
 drt --> rdn
 end

 style vpc fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
Loading
Resource Role Cardinality
aws_vpc.this Keystone VPC 1
aws_vpc_ipv4_cidr_block_association.secondary Secondary IPv4 CIDRs per secondary_cidr_blocks entry
aws_default_security_group.this Adopt + lock default SG to deny-all 1
aws_internet_gateway.this Public ingress/egress 1 (when public subnets exist)
aws_subnet.public / .private / .database Three subnet tiers per subnet-map entry
aws_eip.nat Elastic IPs backing NAT per NAT gateway
aws_nat_gateway.this Private-subnet egress per nat_gateway_mode
aws_route_table.public / .private / .database Per-tier routing shared / per-AZ
aws_route.public_internet / .private_nat / .database_nat Default routes per route table
aws_route_table_association.* Subnet ↔ route-table bindings per subnet
aws_vpc_dhcp_options.this (+ association) Custom DHCP option set 0–1
aws_flow_log.this VPC Flow Logs 0–1

✅ Provider / Versions

Requirement Version
Terraform >= 1.12.0
hashicorp/aws >= 6.0, < 7.0

No provider {} block is declared inside the module — the caller's configured provider (and its region/credentials) is inherited.


🔑 Required IAM Permissions

Least-privilege actions the Terraform identity needs to create, read, update, and delete everything this module manages:

Action Required for Notes
ec2:CreateVpc, ec2:DeleteVpc, ec2:DescribeVpcs, ec2:ModifyVpcAttribute VPC lifecycle + DNS / NAU attributes ModifyVpcAttribute sets DNS support/hostnames
ec2:AssociateVpcCidrBlock, ec2:DisassociateVpcCidrBlock Secondary IPv4 CIDRs, Amazon IPv6 /56 Only when used
ec2:CreateSubnet, ec2:DeleteSubnet, ec2:DescribeSubnets, ec2:ModifySubnetAttribute Subnet tiers ModifySubnetAttribute sets map_public_ip_on_launch, DNS64, etc.
ec2:CreateRouteTable, ec2:DeleteRouteTable, ec2:CreateRoute, ec2:DeleteRoute, ec2:AssociateRouteTable, ec2:DisassociateRouteTable, ec2:DescribeRouteTables Routing
ec2:CreateInternetGateway, ec2:AttachInternetGateway, ec2:DetachInternetGateway, ec2:DeleteInternetGateway, ec2:DescribeInternetGateways Internet Gateway
ec2:CreateNatGateway, ec2:DeleteNatGateway, ec2:DescribeNatGateways NAT gateways Skipped when nat_gateway_mode = "none"
ec2:AllocateAddress, ec2:ReleaseAddress, ec2:DescribeAddresses NAT Elastic IPs EIP release lags NAT delete
ec2:CreateDhcpOptions, ec2:DeleteDhcpOptions, ec2:AssociateDhcpOptions, ec2:DescribeDhcpOptions Custom DHCP option set Only when dhcp_options set
ec2:CreateFlowLogs, ec2:DeleteFlowLogs, ec2:DescribeFlowLogs VPC Flow Logs Default ON
ec2:RevokeSecurityGroupIngress, ec2:RevokeSecurityGroupEgress, ec2:DescribeSecurityGroups Lock the default SG to deny-all aws_default_security_group strips rules
ec2:CreateTags, ec2:DeleteTags Tagging every resource
iam:PassRole Pass the Flow Log delivery role to vpc-flow-logs.amazonaws.com CloudWatch Logs destination only

ℹ️ The module itself creates no IAM role and no service-linked role. Flow Log delivery permissions (logs:CreateLogDelivery / s3:PutObject on the target) live on the destination, not on the Terraform identity — they are granted via the delivery role or the bucket/log-group resource policy in sibling modules.


📋 AWS Prerequisites

  • Service-linked roles: None required for core VPC resources.
  • Flow Logs (default ON): A destination must already exist —
  • CloudWatch Logs: a Log Group plus an IAM role assumable by vpc-flow-logs.amazonaws.com (wire flow_log_destination_arn + flow_log_iam_role_arn).
  • S3: a bucket with a delivery policy permitting delivery.logs.amazonaws.com (wire flow_log_destination_arn; no IAM role needed).
  • Firehose: an Amazon Data Firehose delivery stream.
  • If enable_flow_logs = true but flow_log_destination_arn is null, no flow log is created (no error) — wire a destination to activate it.
  • CMK (optional): SSE-KMS on the Flow Log target requires a KMS key whose key policy grants the delivery service (delivery.logs.amazonaws.com for CloudWatch/S3) use of the key. Encryption is configured on the destination, not in this module.
  • CIDR planning: Use an RFC 1918 block. Subnet cidr_block and availability_zone are force-new — plan the layout up front.
  • Region: No us-east-1 constraint — VPC is a regional service; the module inherits the caller's provider region. (No global-service gotcha applies here.)
  • Service quotas (per Region, soft unless noted): 5 VPCs · 5 Elastic IPs (NAT gateways consume EIPs) · 5 NAT gateways per AZ · 200 subnets per VPC · 5 IPv4 CIDR blocks per VPC. Request increases via Service Quotas where needed.

📁 Module Structure

tf-mod-aws-vpc/
├── providers.tf # terraform{} + required_providers (aws >= 6.0, < 7.0); no provider block
├── variables.tf # name, cidr_block, subnet tiers, NAT topology, DHCP, flow logs, tags
├── main.tf # locals (topology) + aws_vpc.this and full network fabric
├── outputs.tf # id + arn (+ aliases), subnet/route-table maps, gateways, tags_all
├── README.md # this file
└── SCOPE.md # in/out-of-scope, IAM, prerequisites, emits, gotchas

⚙️ Quick Start

module "vpc" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"

  name       = "casey-core"
  cidr_block = "10.0.0.0/16"

  public_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.0.0.0/24" }
    b = { availability_zone = "us-east-1b", cidr_block = "10.0.1.0/24" }
  }
  private_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.0.10.0/24" }
    b = { availability_zone = "us-east-1b", cidr_block = "10.0.11.0/24" }
  }
  database_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.0.20.0/24" }
    b = { availability_zone = "us-east-1b", cidr_block = "10.0.21.0/24" }
  }

  nat_gateway_mode = "per_az" # production HA

  tags = {
    Environment = "prod"
    CostCenter  = "infra"
  }
}

⚠️ Pin the source with ?ref=v1.0.0 — never a branch. To activate the secure-by-default Flow Logs, wire flow_log_destination_arn (and flow_log_iam_role_arn for the CloudWatch path) from sibling modules — see the Example Library.


🔌 Cross-Module Contract

Consumes

This is a foundation module — it consumes no sibling module. Its only optional inputs are by-reference Flow Log targets:

Input Type Source module
flow_log_destination_arn string (ARN) tf-mod-aws-cloudwatch-log-group or tf-mod-aws-s3-bucket
flow_log_iam_role_arn string (ARN) tf-mod-aws-iam-role (CloudWatch destination only)

Emits

Output Description Consumed by
id / vpc_id VPC id nearly every networking / compute / db module
arn / vpc_arn VPC ARN IAM / SCP / policy conditions scoped to the VPC
cidr_block Primary IPv4 CIDR peering, route, SG-rule design
public_subnet_ids Map of public subnet ids tf-mod-aws-lb, NAT placement
private_subnet_ids Map of private subnet ids tf-mod-aws-ecs-service, tf-mod-aws-eks, tf-mod-aws-rds, Lambda
database_subnet_ids Map of database subnet ids tf-mod-aws-rds / tf-mod-aws-rds-aurora subnet groups
public_route_table_ids / private_route_table_ids / database_route_table_ids Route-table id maps tf-mod-aws-vpc-endpoint, tf-mod-aws-transit-gateway, peering routes
default_security_group_id Locked deny-all default SG id audit / reference (do not attach workloads)
internet_gateway_id / nat_gateway_ids Gateway ids route targets
nat_gateway_public_ips Map of AZ → NAT public IP partner egress allow-listing
tags_all All tags incl. provider default_tags governance / audit

📚 Example Library

1 · Minimal — single public subnet
module "vpc" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"

  name       = "sandbox"
  cidr_block = "10.20.0.0/16"

  public_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.20.0.0/24" }
  }
}
2 · Three tiers across two AZs (cost-optimized single NAT)
module "vpc" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
  name       = "nonprod"
  cidr_block = "10.0.0.0/16"

  public_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.0.0.0/24" }
    b = { availability_zone = "us-east-1b", cidr_block = "10.0.1.0/24" }
  }
  private_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.0.10.0/24" }
    b = { availability_zone = "us-east-1b", cidr_block = "10.0.11.0/24" }
  }
  database_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.0.20.0/24" }
    b = { availability_zone = "us-east-1b", cidr_block = "10.0.21.0/24" }
  }

  nat_gateway_mode = "single" # one shared NAT — lowest cost
}
3 · Production HA — one NAT gateway per AZ
module "vpc" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
  name       = "prod"
  cidr_block = "10.30.0.0/16"

  public_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.30.0.0/24" }
    b = { availability_zone = "us-east-1b", cidr_block = "10.30.1.0/24" }
    c = { availability_zone = "us-east-1c", cidr_block = "10.30.2.0/24" }
  }
  private_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.30.10.0/24" }
    b = { availability_zone = "us-east-1b", cidr_block = "10.30.11.0/24" }
    c = { availability_zone = "us-east-1c", cidr_block = "10.30.12.0/24" }
  }

  nat_gateway_mode = "per_az" # per-AZ private route tables, full HA
}
4 · Fully isolated — no NAT (VPC-endpoint-only egress)
module "vpc" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
  name       = "isolated"
  cidr_block = "10.40.0.0/16"

  private_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.40.10.0/24" }
    b = { availability_zone = "us-east-1b", cidr_block = "10.40.11.0/24" }
  }

  nat_gateway_mode = "none" # private subnets have no default route; reach AWS via endpoints
}
5 · Flow Logs to CloudWatch Logs (secure-default activation)
module "vpc" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
  name       = "prod"
  cidr_block = "10.0.0.0/16"

  private_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.0.10.0/24" }
  }

  enable_flow_logs          = true
  flow_log_destination_type = "cloud-watch-logs"
  flow_log_destination_arn  = module.vpc_flow_log_group.arn # tf-mod-aws-cloudwatch-log-group
  flow_log_iam_role_arn     = module.flow_log_role.arn      # tf-mod-aws-iam-role
  flow_log_traffic_type     = "ALL"
}
6 · Flow Logs to S3 in Parquet, hourly partitions
module "vpc" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
  name       = "audit"
  cidr_block = "10.0.0.0/16"

  private_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.0.10.0/24" }
  }

  enable_flow_logs          = true
  flow_log_destination_type = "s3"
  flow_log_destination_arn  = module.flow_log_bucket.arn # tf-mod-aws-s3-bucket (SSE-KMS on the bucket)
  flow_log_destination_options = {
    file_format        = "parquet"
    per_hour_partition = true
  }
}

💡 SSE-KMS for the logs is configured on the bucket (tf-mod-aws-s3-bucket with a customer-managed key), not here. The bucket's KMS key policy must grant delivery.logs.amazonaws.com.

7 · Customer-managed KMS on the Flow Log destination
# This module references the destination by ARN; encryption lives on the destination.
module "flow_log_group" {
  source      = "git::https://github.com/microsoftexpert/tf-mod-aws-cloudwatch-log-group?ref=v1.0.0"
  name        = "/casey/vpc/flow-logs"
  kms_key_arn = module.kms.arn # tf-mod-aws-kms — CMK whose policy allows delivery.logs.amazonaws.com
}

module "vpc" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
  name       = "prod"
  cidr_block = "10.0.0.0/16"

  private_subnets          = { a = { availability_zone = "us-east-1a", cidr_block = "10.0.10.0/24" } }
  flow_log_destination_arn = module.flow_log_group.arn
  flow_log_iam_role_arn    = module.flow_log_role.arn
}
8 · Disabling Flow Logs (secure-default opt-out — requires exception)
module "vpc" {
  source         = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
  name           = "ephemeral-ci"
  cidr_block     = "10.99.0.0/16"
  public_subnets = { a = { availability_zone = "us-east-1a", cidr_block = "10.99.0.0/24" } }

  enable_flow_logs = false # opt-out of the secure baseline — document the exception
}
9 · Tags — merge with provider default_tags
# Caller's provider block owns default_tags; resource tags win on key conflict.
provider "aws" {
  region = "us-east-1"
  default_tags {
    tags = { Owner = "platform", ManagedBy = "terraform" }
  }
}

module "vpc" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
  name       = "tagged"
  cidr_block = "10.0.0.0/16"

  tags = {
    Environment = "prod"
    Owner       = "network-team" # overrides default_tags Owner on the VPC and children
  }

  public_subnets = {
    a = {
      availability_zone = "us-east-1a"
      cidr_block        = "10.0.0.0/24"
      tags              = { Tier = "public-dmz" } # merged on top of module tags for this subnet
    }
  }
}
# module.vpc.tags_all => { Owner="network-team", ManagedBy="terraform", Environment="prod", Name="tagged" }
10 · Secondary IPv4 CIDRs + Amazon IPv6 /56
module "vpc" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
  name       = "dual-stack"
  cidr_block = "10.0.0.0/16"

  secondary_cidr_blocks            = ["10.1.0.0/16", "10.2.0.0/16"]
  assign_generated_ipv6_cidr_block = true

  public_subnets = {
    a = {
      availability_zone               = "us-east-1a"
      cidr_block                      = "10.0.0.0/24"
      ipv6_cidr_block                 = "2600:1f18:abcd:0::/64" # carve from the assigned /56
      assign_ipv6_address_on_creation = true
    }
  }
}
11 · Custom DHCP option set
module "vpc" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
  name       = "corp"
  cidr_block = "10.0.0.0/16"

  dhcp_options = {
    domain_name         = "corp.financialpartners.internal"
    domain_name_servers = ["10.0.0.2", "AmazonProvidedDNS"]
    ntp_servers         = ["10.0.0.10"]
    netbios_node_type   = "2"
  }
}
12 · Database tier with deliberate NAT egress (patching)
module "vpc" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
  name       = "data"
  cidr_block = "10.0.0.0/16"

  public_subnets   = { a = { availability_zone = "us-east-1a", cidr_block = "10.0.0.0/24" } }
  database_subnets = { a = { availability_zone = "us-east-1a", cidr_block = "10.0.20.0/24" } }

  nat_gateway_mode          = "single"
  database_subnet_nat_route = true # opt the data tier into outbound egress — non-default
}
13 · Private-only VPC (no public DNS hostnames)
module "vpc" {
  source               = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
  name                 = "private-only"
  cidr_block           = "10.50.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = false

  private_subnets  = { a = { availability_zone = "us-east-1a", cidr_block = "10.50.10.0/24" } }
  nat_gateway_mode = "none"
}
14 · End-to-end composition — VPC feeding the rest of the stack (finale)
module "kms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-kms?ref=v1.0.0"
  name   = "casey-app"
}

module "flow_log_group" {
  source      = "git::https://github.com/microsoftexpert/tf-mod-aws-cloudwatch-log-group?ref=v1.0.0"
  name        = "/casey/vpc/flow-logs"
  kms_key_arn = module.kms.arn
}

module "flow_log_role" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-iam-role?ref=v1.0.0"
  name   = "casey-vpc-flow-logs"
  # trust policy: vpc-flow-logs.amazonaws.com
}

module "vpc" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
  name       = "casey-app"
  cidr_block = "10.0.0.0/16"

  public_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.0.0.0/24" }
    b = { availability_zone = "us-east-1b", cidr_block = "10.0.1.0/24" }
  }
  private_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.0.10.0/24" }
    b = { availability_zone = "us-east-1b", cidr_block = "10.0.11.0/24" }
  }
  database_subnets = {
    a = { availability_zone = "us-east-1a", cidr_block = "10.0.20.0/24" }
    b = { availability_zone = "us-east-1b", cidr_block = "10.0.21.0/24" }
  }

  nat_gateway_mode         = "per_az"
  flow_log_destination_arn = module.flow_log_group.arn
  flow_log_iam_role_arn    = module.flow_log_role.arn

  tags = { Environment = "prod", App = "lending-portal" }
}

module "app_sg" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-security-group?ref=v1.0.0"
  name   = "app"
  vpc_id = module.vpc.vpc_id
}

module "alb" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-aws-lb?ref=v1.0.0"
  name       = "app"
  vpc_id     = module.vpc.vpc_id
  subnet_ids = values(module.vpc.public_subnet_ids)
}

module "db" {
  source     = "git::https://github.com/microsoftexpert/tf-mod-aws-rds?ref=v1.0.0"
  identifier = "app"
  subnet_ids = values(module.vpc.database_subnet_ids)
  kms_key_id = module.kms.arn
}

📥 Inputs

ℹ️ High-level grouping:

  • Core: name, cidr_block, instance_tenancy, secondary_cidr_blocks
  • DNS / addressing: enable_dns_support, enable_dns_hostnames, enable_network_address_usage_metrics, assign_generated_ipv6_cidr_block
  • Subnet tiers: public_subnets, private_subnets, database_subnets — each map(object({ availability_zone, cidr_block, ipv6_cidr_block?, map_public_ip_on_launch?, assign_ipv6_address_on_creation?, enable_dns64?, private_dns_hostname_type_on_launch?, tags? }))
  • NAT topology: nat_gateway_mode (single | per_az | none), database_subnet_nat_route
  • DHCP: dhcp_options (object, default null)
  • Flow Logs (secure default): enable_flow_logs, flow_log_destination_type, flow_log_destination_arn, flow_log_iam_role_arn, flow_log_traffic_type, flow_log_max_aggregation_interval, flow_log_log_format, flow_log_destination_options
  • Universal: tags

🧾 Outputs

  • Primary: id / vpc_id, arn / vpc_arn
  • VPC attributes: cidr_block, secondary_cidr_block_associations, ipv6_cidr_block, main_route_table_id, default_network_acl_id, default_security_group_id
  • Subnets: public_subnet_ids / private_subnet_ids / database_subnet_ids, *_subnet_arns, *_subnet_cidr_blocks (maps keyed by subnet name)
  • Route tables: public_route_table_ids, private_route_table_ids, database_route_table_ids
  • Gateways: internet_gateway_id (null when no public subnets), nat_gateway_ids, nat_gateway_public_ips, nat_eip_allocation_ids
  • Conditional (try(...) → null): dhcp_options_id, flow_log_id, flow_log_arn, internet_gateway_id
  • Tags: tags_all

ℹ️ No outputs are marked sensitive — this module emits no secrets.


🧠 Architecture Notes

  • ARN / ID formats:
  • idvpc-0a1b2c3d4e5f...
  • arnarn:aws:ec2:<region>:<account-id>:vpc/<vpc-id> — the cross-resource reference type used by IAM/SCP conditions.
  • Subnets → subnet-...; route tables → rtb-...; IGW → igw-...; NAT → nat-...; flow log → fl-....
  • Force-new / immutable fields: VPC cidr_block and instance_tenancy; subnet cidr_block and availability_zone. Changing any of these destroys and recreates the resource (and everything downstream of a recreated VPC). Secondary CIDRs attach non-destructively.
  • tagstags_alldefault_tags: var.tags is written to every resource's tags argument (subnets/DHCP additionally merge their per-item tags on top). tags_all is the provider-computed union of resource tags over the provider's default_tags; resource tags win on key conflict. default_tags is the caller's provider-block concern and is never set inside the module.
  • Eventual consistency: NAT gateway creation and deletion are slow; EIP release lags the NAT delete. A too-quick re-apply after destroy can hit AddressLimitExceeded / "address still in use." Allow time to settle.
  • Destroy ordering (the #1 failure mode): A VPC will not delete while ENIs remain attached — NAT gateways, endpoints, and ENIs created by RDS / ECS / EKS / Lambda in sibling modules. Tear down consumers first → NAT gateways (EIPs release) → IGW detach → subnets → route tables → VPC. Terraform handles intra-module ordering; cross-module ENIs are the caller's responsibility.
  • Default SG is adopted, not created. aws_default_security_group.this takes over the VPC's existing default SG and strips its rules to deny-all. On terraform destroy it is not deleted — control reverts to AWS, which restores the standard default rules.
  • IGW is conditional. Created only when public_subnets is non-empty; internet_gateway_id is null otherwise. NAT gateways require it and depends_on it for a clean apply.
  • No us-east-1 constraint. VPC is a regional service — the module inherits the caller's provider region and declares no region variable.

🧱 Design Principles

Secure-by-default posture and the explicit opt-out for each:

Hardened default Behavior Opt-out / control
VPC Flow Logs enable_flow_logs = true, ALL traffic enable_flow_logs = false (document the exception); a destination must be wired to activate
Default security group Adopted and locked to deny-all ingress + egress No opt-out — secure invariant; attach workloads to named SGs (tf-mod-aws-security-group)
Database tier isolation No default route — no outbound Internet database_subnet_nat_route = true
Private subnet egress Via NAT only (no direct IGW route) nat_gateway_mode / route configuration
Public IP auto-assign false on private/database tiers, true on public per-subnet map_public_ip_on_launch
Tenancy default (low-cost, secure baseline) instance_tenancy = "dedicated"
Flow Log encryption SSE on the destination; SSE-KMS when the target uses a CMK configured on the destination module, not here

Other principles: one composite owns the full network fabric; three subnet tiers modeled as map(object(...)) for for_each placement (no count); SGs / NACLs / endpoints / peering / TGW deliberately excluded to sibling modules to keep blast radius on the fabric itself.


🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
terraform plan # requires valid AWS credentials (profile / SSO / OIDC) + a region
terraform apply
terraform output

⚠️ plan / apply require valid AWS credentials and a configured region (provider block / AWS_PROFILE / SSO / OIDC). Always pin the module source with ?ref=v1.0.0, never a branch.


🧪 Testing

  • terraform init -backend=false && terraform validate — schema + reference integrity.
  • terraform fmt -check — formatting.
  • terraform plan against a sandbox account — confirm subnet/route/NAT counts match the chosen nat_gateway_mode and that the default SG plans to deny-all.
  • Verify Flow Logs: after apply, confirm flow_log_id is non-null and records appear at the destination.
  • Destroy test in a throwaway account to validate teardown ordering before relying on it in shared environments.

💬 Example Output

Apply complete! Resources: 17 added, 0 changed, 0 destroyed.

Outputs:

arn = "arn:aws:ec2:us-east-1:123456789012:vpc/vpc-0a1b2c3d4e5f67890"
id = "vpc-0a1b2c3d4e5f67890"
cidr_block = "10.0.0.0/16"
public_subnet_ids = { "a" = "subnet-0aaa...", "b" = "subnet-0bbb..." }
private_subnet_ids = { "a" = "subnet-0ccc...", "b" = "subnet-0ddd..." }
database_subnet_ids = { "a" = "subnet-0eee...", "b" = "subnet-0fff..." }
nat_gateway_public_ips = { "us-east-1a" = "52.x.x.x", "us-east-1b" = "54.x.x.x" }
default_security_group_id = "sg-0deadbeef..." # deny-all
tags_all = { "Environment" = "prod", "Name" = "casey-app",... }

🔍 Troubleshooting

  • Tag drift / unexpected tags: Caused by default_tags overlap. tags_all merges resource tags over provider default_tags with resource tags winning — if a tag value differs from what you set, a default_tags entry is colliding. Set the value explicitly in var.tags.
  • Credential-chain failures (NoCredentialProviders / ExpiredToken): No valid credentials resolved. Set AWS_PROFILE, refresh SSO, or confirm the OIDC role assumption in CI. The module never takes credentials as variables.
  • UnauthorizedOperation on create: An IAM action from the Required IAM Permissions table is missing — commonly ec2:CreateFlowLogs or iam:PassRole (CloudWatch flow-log path).
  • Flow log "created" but no records: Wrong destination wiring. For CloudWatch you must supply flow_log_iam_role_arn (a role trusting vpc-flow-logs.amazonaws.com); for S3 the bucket policy must allow delivery.logs.amazonaws.com. Also confirm flow_log_destination_arn is non-null — otherwise no flow log is created at all.
  • DependencyViolation on destroy: ENIs still attached — NAT gateways, endpoints, or ENIs from RDS/ECS/EKS/Lambda in other modules. Destroy consumers first; the VPC deletes last.
  • AddressLimitExceeded / "address still in use" on re-apply: EIP release lags NAT deletion (eventual consistency). Wait and retry, or request an EIP quota increase.
  • Subnet recreates on a CIDR/AZ edit: Expected — cidr_block and availability_zone are force-new. Plan the layout up front; use secondary_cidr_blocks to grow address space non-destructively.
  • No Internet Gateway created: IGW is conditional on public_subnets being non-empty; internet_gateway_id returns null when you define only private/database tiers.

🔗 Related Docs

  • Terraform Registry — hashicorp/aws provider: aws_vpc, aws_subnet, aws_route_table, aws_nat_gateway, aws_flow_log, aws_default_security_group
  • AWS — What is Amazon VPC? (Amazon Virtual Private Cloud User Guide)
  • AWS — Amazon VPC quotas (per-Region limits for VPCs, subnets, EIPs, NAT gateways)
  • AWS — NAT gateways and Internet gateways (User Guide)
  • AWS — Publish flow logs to CloudWatch Logs / S3 (delivery roles and bucket policies)
  • tf-mod-aws-kms, tf-mod-aws-iam-role, tf-mod-aws-cloudwatch-log-group, tf-mod-aws-s3-bucket, tf-mod-aws-security-group (sibling modules)

🧡 "Infrastructure as Code should be standardized, consistent, and secure."

Packages

 
 
 

Contributors

Languages