# opentofu-aws-vpc

A small, readable **AWS VPC module** for OpenTofu (and Terraform): a VPC with
public and private subnets spread across availability zones, an internet
gateway for public egress, and optional NAT gateway(s) for private egress.

It does one job well. It is not a 40-variable framework — it's the module you'd
actually read before running it.

## The problem it solves

Almost every AWS environment starts with the same networking boilerplate: a
VPC, a couple of public subnets, a couple of private ones, an internet gateway,
route tables wired correctly, and a NAT gateway so private hosts can pull
updates. Hand-written, it's a few hundred lines that are easy to get subtly
wrong — a missing route table association, a NAT gateway in the wrong subnet,
public subnets that don't actually assign public IPs.

This module encodes that layout once, with the wiring correct by construction,
so a new environment is a dozen lines of inputs instead of a page of resources
to copy-paste and mis-edit. Change an input, run `plan`, read the diff.

## What it creates

- One **VPC** with DNS support/hostnames on by default.
- One **public subnet per entry** in `public_subnet_cidrs`, each in the AZ at
  the same index, with `map_public_ip_on_launch` and a shared route table to
  the internet gateway.
- One **private subnet per entry** in `private_subnet_cidrs`, each with its own
  route table.
- An **internet gateway**.
- **NAT gateway(s)** with Elastic IPs — either one shared gateway
  (`single_nat_gateway = true`, the cheap default) or one per private subnet
  for AZ-independent egress.

Nothing is created for a list you leave empty: pass no private subnets and you
get a public-only VPC with no NAT gateways or their EIP costs.

## Usage

```hcl
provider "aws" {
  region = "us-east-1"
}

module "vpc" {
  source = "github.com/Slimisjim/opentofu-aws-vpc"

  name       = "prod"
  cidr_block = "10.0.0.0/16"

  azs                  = ["us-east-1a", "us-east-1b"]
  public_subnet_cidrs  = ["10.0.0.0/24", "10.0.1.0/24"]
  private_subnet_cidrs = ["10.0.10.0/24", "10.0.11.0/24"]

  single_nat_gateway = true

  tags = {
    Environment = "prod"
    ManagedBy   = "OpenTofu"
  }
}
```

A runnable version is in [`examples/simple`](examples/simple):

```bash
cd examples/simple
tofu init
tofu plan
```

## Inputs

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `string` | — | Name prefix for the VPC and all child resources |
| `cidr_block` | `string` | `"10.0.0.0/16"` | IPv4 CIDR for the VPC |
| `azs` | `list(string)` | — | AZs to spread subnets across; subnet N lands in AZ N |
| `public_subnet_cidrs` | `list(string)` | `[]` | One public subnet per entry |
| `private_subnet_cidrs` | `list(string)` | `[]` | One private subnet per entry |
| `enable_dns_support` | `bool` | `true` | DNS resolution in the VPC |
| `enable_dns_hostnames` | `bool` | `true` | DNS hostnames for public instances |
| `enable_nat_gateway` | `bool` | `true` | Provision NAT for private egress |
| `single_nat_gateway` | `bool` | `true` | One shared NAT vs. one per AZ |
| `tags` | `map(string)` | `{}` | Tags applied to every resource |

Inputs are validated: CIDRs must parse, at least one AZ is required, and `name`
is length-checked — so bad input fails at `plan`, not mid-apply.

## Outputs

| Name | Description |
|------|-------------|
| `vpc_id` | ID of the VPC |
| `vpc_cidr_block` | CIDR block of the VPC |
| `internet_gateway_id` | ID of the internet gateway |
| `public_subnet_ids` | Public subnet IDs, in AZ order |
| `private_subnet_ids` | Private subnet IDs, in AZ order |
| `public_route_table_id` | Shared public route table ID |
| `private_route_table_ids` | Per-subnet private route table IDs |
| `nat_gateway_ids` | NAT gateway IDs (empty when NAT is disabled) |

## Design choices worth knowing

- **Index-aligned lists.** Subnet, CIDR, and AZ share an index — `public_subnet_cidrs[1]`
  lives in `azs[1]`. Simple to reason about; keep the lists the same length as
  your AZ list.
- **`single_nat_gateway` defaults to `true`.** A NAT gateway is one of the
  quietest line items on an AWS bill. The default trades AZ-independent egress
  for cost; flip it off for production workloads that need per-AZ resilience.
- **No provider inside the module.** The module declares its provider
  *requirements* but configures no `provider` block, so callers stay in control
  of region, credentials, and aliases — the standard for a reusable module.
- **Validated inputs.** Bad CIDRs or an empty AZ list are rejected up front.

## Requirements

| | Version |
|---|---------|
| OpenTofu / Terraform | >= 1.6.0 |
| AWS provider | >= 5.0 |

## CI

[`.github/workflows/ci.yml`](.github/workflows/ci.yml) runs `tofu fmt -check`,
`tofu validate` (root module and example), and `tflint` on every push and pull
request. Validation needs no AWS credentials.

## License

MIT — see [LICENSE](LICENSE).
