From 9420ed81ccdadebe2e6dd228d262152646becf19 Mon Sep 17 00:00:00 2001 From: Osifowora Date: Mon, 19 Oct 2026 12:12:09 +0000 Subject: [PATCH] docs(infra): add READMEs to the rds, redis, secrets, and vpc Terraform modules Each of the four modules under infra/terraform/modules/ had no module-level README, so consuming them meant reading every .tf file first. Add a README to each, following the formatting conventions already used in docs/: an H1 title, an Overview, tables for the resources, inputs, and outputs, a copy-pasteable usage example, and operational notes. Documented per module: - vpc: subnet/AZ count constraints, nat_gateway_count bounds and cost, public vs. private subnet roles, and the modules that consume its outputs. - rds: the empty allowed_cidr_blocks default, master_password landing in state, deletion_protection and skip_final_snapshot behaviour, and writer vs. reader endpoints. - redis: replica_count including the primary, the two-cache-cluster minimum for failover and Multi-AZ, the rediss:// requirement under transit encryption, and auth_token constraints. - secrets: the placeholder-value contract, out-of-band injection, the read policy scope, and the manual rotation procedure. Docs-only change; no .tf file was modified. Closes #320 Closes #321 Closes #322 Closes #323 --- infra/terraform/modules/rds/README.md | 135 +++++++++++++++++++++ infra/terraform/modules/redis/README.md | 140 ++++++++++++++++++++++ infra/terraform/modules/secrets/README.md | 113 +++++++++++++++++ infra/terraform/modules/vpc/README.md | 118 ++++++++++++++++++ 4 files changed, 506 insertions(+) create mode 100644 infra/terraform/modules/rds/README.md create mode 100644 infra/terraform/modules/redis/README.md create mode 100644 infra/terraform/modules/secrets/README.md create mode 100644 infra/terraform/modules/vpc/README.md diff --git a/infra/terraform/modules/rds/README.md b/infra/terraform/modules/rds/README.md new file mode 100644 index 00000000..1360fd22 --- /dev/null +++ b/infra/terraform/modules/rds/README.md @@ -0,0 +1,135 @@ +# Terraform `rds` Module + +Provisions an Aurora PostgreSQL cluster with its own subnet group and security +group, ready for the EziAgric backend to connect to. + +## Overview + +The module creates a single Aurora cluster plus a configurable number of +instances, places them in the private subnets of the [`vpc`](../vpc/README.md) +module, and locks network access down with a dedicated security group that only +admits the configured CIDR blocks on the database port. + +The database is the system of record for off-chain trade metadata. See +[`docs/data-model-relationships.md`](../../../../docs/data-model-relationships.md) +for the schema context and +[`docs/pii-encryption.md`](../../../../docs/pii-encryption.md) for how PII is +protected at rest. + +## Resources + +| Resource | Type | Purpose | +|---|---|---| +| `aws_db_subnet_group.this` | `aws_db_subnet_group` | Places the cluster across the supplied private subnets | +| `aws_security_group.this` | `aws_security_group` | Ingress on `port` from `allowed_cidr_blocks`; unrestricted egress. Uses `create_before_destroy` so replacements do not drop traffic | +| `aws_rds_cluster.this` | `aws_rds_cluster` | Aurora cluster: engine, credentials, backup retention and window, subnet group, security group, deletion protection | +| `aws_rds_cluster_instance.this` | `aws_rds_cluster_instance` | `replica_count` instances in the cluster, identified `-rds-instance-` | + +Every resource is tagged `Name` and `Environment`. + +## Inputs + +| Name | Type | Default | Required | Description | +|---|---|---|---|---| +| `project_name` | `string` | — | yes | Project name. Used as the resource name prefix and the `Name` tag. | +| `environment` | `string` | — | yes | Environment name (e.g. `dev`, `staging`). Applied as the `Environment` tag. | +| `vpc_id` | `string` | — | yes | VPC ID in which the security group is created. Use `module.vpc.vpc_id`. | +| `subnet_ids` | `list(string)` | — | yes | Subnet IDs for the DB subnet group. Use `module.vpc.private_subnet_ids`. | +| `allowed_cidr_blocks` | `list(string)` | `[]` | no | CIDR blocks allowed to reach the database on `port`. **Empty means no inbound access at all.** | +| `engine` | `string` | `"aurora-postgresql"` | no | Database engine. | +| `engine_version` | `string` | `"15.4"` | no | Engine version. | +| `instance_class` | `string` | `"db.t3.medium"` | no | Instance class for every cluster instance. | +| `database_name` | `string` | — | yes | Name of the initial database created in the cluster. | +| `master_username` | `string` | — | yes | Master username. Marked sensitive in the calling environment. | +| `master_password` | `string` | — | yes | Master password. **Sensitive** — never commit it to a `*.tfvars` file. | +| `port` | `number` | `5432` | no | Database port; also the port opened in the security group. | +| `replica_count` | `number` | `1` | no | Number of instances in the cluster. `1` = writer only, `2` = writer + one reader. | +| `backup_retention_period` | `number` | `7` | no | Automated backup retention in days. | +| `preferred_backup_window` | `string` | `"03:00-04:00"` | no | Preferred daily backup window (UTC). | +| `skip_final_snapshot` | `bool` | `false` | no | Skip the final snapshot when the cluster is destroyed. | +| `deletion_protection` | `bool` | `true` | no | Block deletion of the cluster while enabled. | + +## Outputs + +| Name | Type | Description | +|---|---|---| +| `cluster_endpoint` | `string` | Writer endpoint — use this for application connections. | +| `cluster_reader_endpoint` | `string` | Reader endpoint — use this for read-only queries and reporting. | +| `cluster_id` | `string` | ID of the Aurora cluster. | +| `instance_ids` | `list(string)` | IDs of the cluster instances. | + +## Usage + +```hcl +module "rds" { + source = "../../modules/rds" + + project_name = "amana" + environment = "dev" + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnet_ids + allowed_cidr_blocks = [module.vpc.vpc_cidr] + engine = "aurora-postgresql" + engine_version = "15.4" + instance_class = "db.t3.medium" + database_name = "amana_dev" + master_username = var.db_master_username + master_password = var.db_master_password + port = 5432 + replica_count = 1 + backup_retention_period = 7 + preferred_backup_window = "03:00-04:00" + skip_final_snapshot = true + deletion_protection = false +} +``` + +A live example lives in +[`infra/terraform/environments/dev/main.tf`](../../environments/dev/main.tf); +staging uses the same block with larger instances, a 30-day backup window, and +deletion protection enabled. + +Point the backend at the cluster with the writer endpoint: + +``` +postgresql://:@:5432/ +``` + +## Operational notes + +- **`allowed_cidr_blocks` defaults to `[]`.** With an empty list the security + group has no inbound rule at all and nothing — not even the EKS node group — + can reach the database. Set it explicitly (the environments use + `[module.vpc.vpc_cidr]`). +- **The master password lands in Terraform state.** The password is stored on + the cluster resource, so the state file must stay encrypted. The environment + backends use an S3 bucket with `encrypt = true` and a DynamoDB lock table. + Prefer injecting the value via `TF_VAR_db_master_password` from a secret + store over a committed `*.tfvars` file. +- **`deletion_protection` defaults to `true`.** Set it to `false` (and let the + change apply) before destroying the cluster, otherwise the destroy fails. +- **`skip_final_snapshot` should stay `false` outside dev.** The dev + environment sets it to `true` so that tearing down the stack does not leave + paid-for snapshots behind. +- **Aurora readers scale out, they do not replace the writer.** `replica_count` + counts cluster instances; the cluster always keeps one writer. Use + `cluster_reader_endpoint` for analytics so reporting does not contend with + trade traffic. +- **Backups are managed by the cluster**, not by the database. Restore + procedures are in + [`docs/runbooks/database-migration.md`](../../../../docs/runbooks/database-migration.md). +- **The security group allows all egress** (`0.0.0.0/0`). Egress is restricted + at the network-policy layer in the Kubernetes manifests, not here. + +## Related + +- [`../vpc/README.md`](../vpc/README.md) — provides `vpc_id` and + `private_subnet_ids` +- [`infra/terraform/environments/dev/main.tf`](../../environments/dev/main.tf) — + dev wiring +- [`docs/secrets-policy.md`](../../../../docs/secrets-policy.md) — where the + database credentials are expected to come from +- [`docs/data-model-relationships.md`](../../../../docs/data-model-relationships.md) — + what lives in this database +- [`docs/runbooks/database-migration.md`](../../../../docs/runbooks/database-migration.md) — + migration and restore procedures diff --git a/infra/terraform/modules/redis/README.md b/infra/terraform/modules/redis/README.md new file mode 100644 index 00000000..6e458abf --- /dev/null +++ b/infra/terraform/modules/redis/README.md @@ -0,0 +1,140 @@ +# Terraform `redis` Module + +Provisions an ElastiCache for Redis replication group — a Multi-AZ, encrypted, +password-protected cache cluster — together with its own subnet group and +security group. + +## Overview + +Redis backs the backend's queues, distributed locks, cache, and rate limiting. +This module creates the managed cluster that those consumers talk to, placed in +the private subnets of the [`vpc`](../vpc/README.md) module and reachable only +from the CIDR blocks in `allowed_cidr_blocks`. + +The expected behaviour of every consumer during a Redis outage is specified in +[`docs/redis-resilience.md`](../../../../docs/redis-resilience.md), and the +maintenance-window procedure lives in +[`docs/runbooks/redis-maintenance.md`](../../../../docs/runbooks/redis-maintenance.md). + +## Resources + +| Resource | Type | Purpose | +|---|---|---| +| `aws_security_group.this` | `aws_security_group` | Ingress on `port` from `allowed_cidr_blocks`; unrestricted egress. Uses `create_before_destroy` so replacements do not drop traffic | +| `aws_elasticache_subnet_group.this` | `aws_elasticache_subnet_group` | Places the replication group across the supplied private subnets | +| `aws_elasticache_replication_group.this` | `aws_elasticache_replication_group` | The Redis replication group: node type, cluster count, engine, parameter group, encryption, auth token, and snapshot settings | + +Every resource is tagged `Name` and `Environment`. + +## Inputs + +| Name | Type | Default | Required | Description | +|---|---|---|---|---| +| `project_name` | `string` | — | yes | Project name. Used as the replication group ID and the `Name` tag. | +| `environment` | `string` | — | yes | Environment name (e.g. `dev`, `staging`). Applied as the `Environment` tag. | +| `vpc_id` | `string` | — | yes | VPC ID in which the security group is created. Use `module.vpc.vpc_id`. | +| `subnet_ids` | `list(string)` | — | yes | Subnet IDs for the cache subnet group. Use `module.vpc.private_subnet_ids`. | +| `allowed_cidr_blocks` | `list(string)` | `[]` | no | CIDR blocks allowed to reach Redis on `port`. **Empty means no inbound access at all.** | +| `engine` | `string` | `"redis"` | no | Cache engine. | +| `engine_version` | `string` | `"7.0"` | no | Engine version. | +| `node_type` | `string` | `"cache.t3.medium"` | no | Cache node type for every node in the group. | +| `port` | `number` | `6379` | no | Redis port; also the port opened in the security group. | +| `replica_count` | `number` | `2` | no | Number of cache clusters (`number_cache_clusters`). **Includes the primary** — the default of `2` is one primary plus one replica. | +| `parameter_group_name` | `string` | `"default.redis7"` | no | Parameter group applied to the group. Must already exist. | +| `automatic_failover_enabled` | `bool` | `true` | no | ElastiCache promotes a replica automatically if the primary fails. | +| `multi_az_enabled` | `bool` | `true` | no | Spread the nodes across availability zones. | +| `at_rest_encryption_enabled` | `bool` | `true` | no | Encrypt the data on disk and the snapshots. | +| `transit_encryption_enabled` | `bool` | `true` | no | Encrypt client ↔ Redis traffic (TLS). | +| `auth_token` | `string` | — | yes | Password (16–128 printable characters) clients must present. **Sensitive** — never commit it. | +| `snapshot_retention_limit` | `number` | `5` | no | Number of days snapshots are retained. `0` disables snapshots. | +| `snapshot_window` | `string` | `"02:00-03:00"` | no | Daily snapshot window (UTC). | + +## Outputs + +| Name | Type | Description | +|---|---|---| +| `primary_endpoint` | `string` | Primary (writer) endpoint — use this for application connections. | +| `reader_endpoint` | `string` | Reader endpoint for read-only clients. | +| `replication_group_id` | `string` | ID of the replication group. | +| `port` | `number` | Port the group is listening on. | + +## Usage + +```hcl +module "redis" { + source = "../../modules/redis" + + project_name = "amana" + environment = "dev" + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnet_ids + allowed_cidr_blocks = [module.vpc.vpc_cidr] + engine = "redis" + engine_version = "7.0" + node_type = "cache.t3.medium" + port = 6379 + replica_count = 2 + parameter_group_name = "default.redis7" + automatic_failover_enabled = true + multi_az_enabled = true + at_rest_encryption_enabled = true + transit_encryption_enabled = true + auth_token = var.redis_auth_token + snapshot_retention_limit = 5 + snapshot_window = "02:00-03:00" +} +``` + +A live example lives in +[`infra/terraform/environments/dev/main.tf`](../../environments/dev/main.tf); +staging uses the same block with larger nodes, three cache clusters, and a +14-day snapshot window. + +Point the backend at the group with the primary endpoint, carrying the auth +token as the URL password and using `rediss://` because transit encryption is +on: + +``` +rediss://:@:6379 +``` + +The backend reads this from `REDIS_URL` +(see [`backend/src/config/env.ts`](../../../../backend/src/config/env.ts)). + +## Operational notes + +- **`replica_count` includes the primary.** It is passed straight to + `number_cache_clusters`, so the default `2` means one primary plus one + replica — not two replicas. +- **Failover and Multi-AZ need at least two cache clusters.** AWS rejects + `automatic_failover_enabled = true` or `multi_az_enabled = true` on a group + with a single node. Keep `replica_count >= 2` when either flag is on. +- **`allowed_cidr_blocks` defaults to `[]`.** With an empty list the security + group has no inbound rule and no client can connect. Set it explicitly (the + environments use `[module.vpc.vpc_cidr]`). +- **`transit_encryption_enabled = true` requires a TLS client.** The backend + connects with `ioredis`, so `REDIS_URL` must use the `rediss://` scheme — a + plain `redis://` URL will fail the handshake against an encrypted group. +- **`auth_token` is required and sensitive.** The token is stored in Terraform + state, so the state file must stay encrypted. The AWS provider also rejects + tokens shorter than 16 characters or containing `/`, `"`, or `@`. +- **The auth token cannot be rotated in place** on a replication group — + changing it replaces the group, which is a disruptive operation. Plan + maintenance accordingly; the runbook covers the procedure. +- **Snapshots are enabled by default** (`snapshot_retention_limit = 5`). Setting + it to `0` disables snapshots entirely and removes the last restore point. +- **The security group allows all egress** (`0.0.0.0/0`). Egress is restricted + at the network-policy layer in the Kubernetes manifests, not here. + +## Related + +- [`../vpc/README.md`](../vpc/README.md) — provides `vpc_id` and + `private_subnet_ids` +- [`infra/terraform/environments/dev/main.tf`](../../environments/dev/main.tf) — + dev wiring +- [`docs/redis-resilience.md`](../../../../docs/redis-resilience.md) — expected + behaviour per consumer when Redis is unavailable +- [`docs/runbooks/redis-maintenance.md`](../../../../docs/runbooks/redis-maintenance.md) — + maintenance-window procedure that guarantees no payout risk +- [`docs/idempotency-redis-roundtrips.md`](../../../../docs/idempotency-redis-roundtrips.md) — + how the idempotency middleware uses this cluster diff --git a/infra/terraform/modules/secrets/README.md b/infra/terraform/modules/secrets/README.md new file mode 100644 index 00000000..1ec5eb91 --- /dev/null +++ b/infra/terraform/modules/secrets/README.md @@ -0,0 +1,113 @@ +# Terraform `secrets` Module + +Creates the AWS Secrets Manager container that holds `ADMIN_SECRET_KEY` — the +Stellar Soroban signing key used by backend admin operations — and attaches the +IAM policy that lets the EKS node role read it. + +## Overview + +The module is built around a hard rule: **the secret value is never written to +Terraform state, Git, Kubernetes manifests, or logs.** Terraform creates the +secret container with a placeholder version; the real value is injected out of +band with the AWS CLI immediately after the first apply. + +The full provisioning, rotation, and troubleshooting procedure is documented in +[`docs/admin-secret-management.md`](../../../../docs/admin-secret-management.md), +and the wider inventory in +[`docs/secrets-policy.md`](../../../../docs/secrets-policy.md). + +## Resources + +| Resource | Type | Purpose | +|---|---|---| +| `aws_secretsmanager_secret.admin_secret_key` | `aws_secretsmanager_secret` | Secret container named `--admin-secret-key` | +| `aws_secretsmanager_secret_version.admin_secret_key` | `aws_secretsmanager_secret_version` | Initial placeholder version (`REPLACE_WITH_ADMIN_SECRET_KEY`) with `ignore_changes = [secret_string]`, so Terraform never overwrites the injected value | +| `aws_iam_policy_document.admin_secret_key_read` (data) | `aws_iam_policy_document` | Policy document allowing `secretsmanager:GetSecretValue` and `secretsmanager:DescribeSecret` on this secret only | +| `aws_iam_policy.admin_secret_key_read` | `aws_iam_policy` | Customer-managed IAM policy carrying that document | +| `aws_iam_role_policy_attachment.admin_secret_key_read` | `aws_iam_role_policy_attachment` | Attaches the policy to the EKS node role | + +## Inputs + +| Name | Type | Default | Required | Description | +|---|---|---|---|---| +| `project_name` | `string` | — | yes | Project name. Used as the name prefix of the secret and the IAM policy. | +| `environment` | `string` | — | yes | Environment name (e.g. `dev`, `staging`). Part of the secret name and the `Environment` tag. | +| `eks_node_role_name` | `string` | — | yes | IAM role of the EKS worker nodes, as a **name or an ARN** — the environments pass `module.eks.node_role_arn`. The read policy is attached to it. | + +## Outputs + +| Name | Type | Description | +|---|---|---| +| `secret_arn` | `string` | ARN of the secret. Use it in a K8s `ExternalSecret` or the Secrets Store CSI driver. | +| `secret_name` | `string` | Name of the secret, for use with `aws secretsmanager` CLI commands. | +| `secret_read_policy_arn` | `string` | ARN of the IAM policy granting read access. | + +None of the outputs expose the secret value. + +## Usage + +```hcl +module "secrets" { + source = "../../modules/secrets" + + project_name = "amana" + environment = "dev" + eks_node_role_name = module.eks.node_role_arn +} +``` + +A live example lives in +[`infra/terraform/environments/dev/main.tf`](../../environments/dev/main.tf); +staging wires the same three inputs. + +After the first apply, inject the real signing key — the placeholder is +rejected by the backend's health check: + +```bash +aws secretsmanager put-secret-value \ + --secret-id "$(terraform output -raw secret_name)" \ + --secret-string "S" +``` + +## Operational notes + +- **Terraform creates the container, not the value.** The first version holds + the literal placeholder `REPLACE_WITH_ADMIN_SECRET_KEY`. The + `ignore_changes = [secret_string]` lifecycle rule means subsequent applies + leave whatever you injected in place — Terraform will not reset it to the + placeholder. +- **Inject the real value immediately after the first apply.** Until you do, + the secret exists but carries a non-functional value, and the backend's + `adminSigningKey` health check fails. +- **The real value never enters Terraform state.** It is written through the + AWS CLI, not through Terraform, so it is absent from the state file, from + plan output, and from CI logs. +- **Read access is scoped to the EKS node role and to this one secret.** The + policy grants only `GetSecretValue` and `DescribeSecret` on the secret ARN. + Pods reach the value through the node role, so the pod's service account + needs no additional IAM binding. +- **Rotation is out of band.** Generate a new Stellar keypair, call + `put-secret-value` again, and restart the backend pods. The steps and the + verification command are in + [`docs/admin-secret-management.md`](../../../../docs/admin-secret-management.md#rotation). +- **Automatic rotation is not implemented in this module.** The module creates + no `aws_secretsmanager_secret_rotation` resource — until one is added, rotation + is the manual procedure above. + [`docs/admin-secret-management.md`](../../../../docs/admin-secret-management.md#automated-rotation-optional) + describes the shape of the rotation Lambda. +- **The secret name is environment-scoped.** It resolves to + `amana--admin-secret-key`, matching the names used by the + Kubernetes manifests and the rotation docs. + +## Related + +- [`../eks`](../eks/) — provides the `node_role_arn` used to attach the read + policy +- [`infra/terraform/environments/dev/main.tf`](../../environments/dev/main.tf) — + dev wiring +- [`docs/admin-secret-management.md`](../../../../docs/admin-secret-management.md) — + provisioning, rotation, and troubleshooting for `ADMIN_SECRET_KEY` +- [`docs/secrets-policy.md`](../../../../docs/secrets-policy.md) — secrets + inventory, owners, and rotation cadence +- [`docs/admin-secret-rotation.md`](../../../../docs/admin-secret-rotation.md) — + rotation automation diff --git a/infra/terraform/modules/vpc/README.md b/infra/terraform/modules/vpc/README.md new file mode 100644 index 00000000..1509ce5e --- /dev/null +++ b/infra/terraform/modules/vpc/README.md @@ -0,0 +1,118 @@ +# Terraform `vpc` Module + +Provisions the AWS network foundation for an EziAgric environment: one VPC, an +Internet gateway, public and private subnets spread across availability zones, +and NAT gateways that give the private subnets outbound internet access. + +## Overview + +This module is the root of the Terraform dependency graph — every other module +in `infra/terraform/modules/` consumes its outputs: + +| Consumer | Uses | +|---|---| +| [`rds`](../rds/README.md) | `vpc_id`, `private_subnet_ids` — Aurora cluster placement and its security group | +| [`redis`](../redis/README.md) | `vpc_id`, `private_subnet_ids` — ElastiCache replication group placement and its security group | +| [`eks`](../eks/) | `vpc_id`, `private_subnet_ids` — control plane VPC config and node group subnets | +| [`admin_isolation`](../admin_isolation/) | `vpc_id` — security group that restricts admin endpoint access | + +The module is deliberately network-only. It creates **no security groups** of +its own: data-store security groups live in the `rds` and `redis` modules, and +admin endpoint isolation lives in the `admin_isolation` module. + +## Resources + +| Resource | Type | Purpose | +|---|---|---| +| `aws_vpc.this` | `aws_vpc` | VPC with DNS support and DNS hostnames enabled | +| `aws_internet_gateway.this` | `aws_internet_gateway` | IGW attached to the VPC; terminates the public route | +| `aws_subnet.public` | `aws_subnet` | One public subnet per entry in `public_subnet_cidrs`; `map_public_ip_on_launch = true` | +| `aws_subnet.private` | `aws_subnet` | One private subnet per entry in `private_subnet_cidrs`; no public IP mapping | +| `aws_eip.nat` | `aws_eip` | Elastic IP for each NAT gateway (VPC domain) | +| `aws_nat_gateway.this` | `aws_nat_gateway` | NAT gateway in a public subnet; provides egress for private subnets | +| `aws_route_table.public` | `aws_route_table` | Public route table — `0.0.0.0/0` → internet gateway | +| `aws_route_table.private` | `aws_route_table` | One private route table per NAT gateway — `0.0.0.0/0` → NAT gateway | +| `aws_route_table_association.public` | `aws_route_table_association` | Binds every public subnet to the public route table | +| `aws_route_table_association.private` | `aws_route_table_association` | Binds every private subnet to a private route table (round-robin over NAT gateways) | + +Every resource is tagged `Name` and `Environment`. + +## Inputs + +| Name | Type | Default | Required | Description | +|---|---|---|---|---| +| `project_name` | `string` | — | yes | Project name. Used as the resource name prefix and the `Name` tag. | +| `environment` | `string` | — | yes | Environment name (e.g. `dev`, `staging`). Applied as the `Environment` tag. | +| `region` | `string` | — | yes | AWS region. Declared for parity with the other modules; the module body does not reference it, so the region is taken from the `aws` provider in the calling environment. | +| `vpc_cidr` | `string` | — | yes | CIDR block for the VPC. Must contain every public and private subnet CIDR. | +| `public_subnet_cidrs` | `list(string)` | — | yes | CIDR blocks for the public subnets. One subnet is created per entry. | +| `private_subnet_cidrs` | `list(string)` | — | yes | CIDR blocks for the private subnets. One subnet is created per entry. | +| `availability_zones` | `list(string)` | — | yes | Availability zones, matched **positionally** against both subnet CIDR lists. | +| `nat_gateway_count` | `number` | `1` | no | Number of NAT gateways, and therefore the number of private route tables. | + +## Outputs + +| Name | Type | Description | +|---|---|---| +| `vpc_id` | `string` | ID of the VPC. Pass to `rds`, `redis`, `eks`, and `admin_isolation`. | +| `public_subnet_ids` | `list(string)` | IDs of the public subnets, in `public_subnet_cidrs` order. | +| `private_subnet_ids` | `list(string)` | IDs of the private subnets, in `private_subnet_cidrs` order. | +| `nat_gateway_ids` | `list(string)` | IDs of the NAT gateways, in creation order. | + +## Usage + +```hcl +module "vpc" { + source = "../../modules/vpc" + + project_name = "amana" + environment = "dev" + region = var.region + vpc_cidr = "10.0.0.0/16" + public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"] + private_subnet_cidrs = ["10.0.10.0/24", "10.0.11.0/24"] + availability_zones = ["us-east-1a", "us-east-1b"] + nat_gateway_count = 1 +} +``` + +A live example lives in +[`infra/terraform/environments/dev/main.tf`](../../environments/dev/main.tf) +and its staging counterpart in +[`infra/terraform/environments/staging/main.tf`](../../environments/staging/main.tf). + +## Operational notes + +- **Subnet/AZ counts must line up.** Subnets are placed with + `availability_zones[count.index]`, so `availability_zones` must contain at + least as many entries as the longest subnet CIDR list. Mismatches fail at + `terraform plan` with an index-out-of-range error. +- **`nat_gateway_count` must be at least 1.** The private route table + association divides by `nat_gateway_count`; a value of `0` fails the plan. +- **NAT gateways are placed in the first N public subnets** + (`aws_subnet.public[count.index]`), so `nat_gateway_count` must not exceed + `length(public_subnet_cidrs)`. +- **NAT gateways are billed per hour.** Use `nat_gateway_count = 1` in dev and + spread them across AZs only where the environment needs AZ-redundant egress + (staging uses one per AZ). +- **Private subnets carry no public IPs.** The EKS node group, Aurora cluster, + and ElastiCache replication group all live here, so they are reachable only + from inside the VPC — this is what keeps admin endpoints and data stores off + the public internet. +- **Public subnets exist for NAT gateways** (and for any future public load + balancer). They are not used by the data-store modules. +- **There is no default route for the VPC itself** beyond the public and + private route tables; on-prem or peering routes are not managed by this + module. + +## Related + +- [`../rds/README.md`](../rds/README.md), + [`../redis/README.md`](../redis/README.md) — data-store modules that run + inside the private subnets created here +- [`../eks/`](../eks/), [`../admin_isolation/`](../admin_isolation/) — the + remaining consumers of `vpc_id` +- [`docs/admin-network-isolation.md`](../../../../docs/admin-network-isolation.md) — + how the admin plane is kept off the public internet +- [`docs/threat-model.md`](../../../../docs/threat-model.md) — network trust + boundaries assumed by the platform