From 90be8485adc2cfad850db54a063cdc2f1644132f Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Fri, 31 Jul 2026 15:22:50 -0400 Subject: [PATCH] Add the Terraform foundation: state bucket, prod root, network bootstrap/ creates the S3 bucket that holds environment state, keeping its own state on local disk because a bucket cannot store the state of its own creation. No DynamoDB table - Terraform 1.10 added S3-native locking and 1.11 deprecated the table, so `use_lockfile = true` is now the supported shape. Versioning is the actual recovery path and costs cents at these file sizes. modules/network is the no-NAT decision made concrete: public subnets across three AZs, an internet gateway, and the free S3 gateway endpoint. Private subnets are deliberately absent rather than present and empty. The default security group is emptied, since a default-open group that nothing references is still waiting for something to reference it. envs/prod composes modules and declares no resources of its own. The provider pins allowed_account_ids, so a wrong-profile apply fails before it plans rather than after it bills. Validated with `terraform validate` against 1.15.8 and AWS provider 6.57 - which checks HCL and required arguments, not whether AWS accepts the combination. Nothing has been planned against an account. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 5 + bootstrap/.terraform.lock.hcl | 26 +++++ bootstrap/README.md | 36 +++++++ bootstrap/main.tf | 146 +++++++++++++++++++++++++++++ bootstrap/outputs.tf | 9 ++ bootstrap/variables.tf | 11 +++ envs/prod/.terraform.lock.hcl | 26 +++++ envs/prod/backend.tf | 23 +++++ envs/prod/main.tf | 23 +++++ envs/prod/outputs.tf | 9 ++ envs/prod/providers.tf | 16 ++++ envs/prod/terraform.tfvars.example | 8 ++ envs/prod/variables.tf | 48 ++++++++++ envs/prod/versions.tf | 13 +++ modules/network/main.tf | 123 ++++++++++++++++++++++++ modules/network/outputs.tf | 19 ++++ modules/network/variables.tf | 37 ++++++++ 17 files changed, 578 insertions(+) create mode 100644 bootstrap/.terraform.lock.hcl create mode 100644 bootstrap/README.md create mode 100644 bootstrap/main.tf create mode 100644 bootstrap/outputs.tf create mode 100644 bootstrap/variables.tf create mode 100644 envs/prod/.terraform.lock.hcl create mode 100644 envs/prod/backend.tf create mode 100644 envs/prod/main.tf create mode 100644 envs/prod/outputs.tf create mode 100644 envs/prod/providers.tf create mode 100644 envs/prod/terraform.tfvars.example create mode 100644 envs/prod/variables.tf create mode 100644 envs/prod/versions.tf create mode 100644 modules/network/main.tf create mode 100644 modules/network/outputs.tf create mode 100644 modules/network/variables.tf diff --git a/.gitignore b/.gitignore index 0a9e7f2..8479ea5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,11 @@ **/.terraform/ .terraform.lock.hcl.bak +# Lock files belong to root modules only - bootstrap/ and envs/*. The ones under +# modules/ are a side effect of validate.sh initialising each module as a root +# to check it, and Terraform ignores them anyway. +modules/*/.terraform.lock.hcl + # State never belongs in git. Environment state lives in S3; bootstrap/ keeps # its own state locally, and losing it costs a `terraform import` rather than # the bucket - see bootstrap/README.md. diff --git a/bootstrap/.terraform.lock.hcl b/bootstrap/.terraform.lock.hcl new file mode 100644 index 0000000..3347a75 --- /dev/null +++ b/bootstrap/.terraform.lock.hcl @@ -0,0 +1,26 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.57.1" + constraints = "~> 6.0" + hashes = [ + "h1:WXndu9uKvbnmspexcbki89ZuGLt2SUyAfZ5GgQUm+QU=", + "zh:2d29e22480a81c21fb3f2fd52f9bd3ca4a82c37f3bb1b1036e881e42cddc75a1", + "zh:33aeb08e9973199b30f8a8e48a58dc67cfb6e32879f7a1c05c521899fe718f53", + "zh:37b7f977a7e7d45ad11d42958bc264873fb34573eee925915038ea05607abc9e", + "zh:41ebdcf4bcd073a01d58505a5f5118b85668de357d2d9f266926923e817a1842", + "zh:43093dfc3559c2c0467c92f48b29ae0221d52e912fce03dd77abd90806fdcc7d", + "zh:63b4252933e828d3590c0c64b827ec0f8955aa52df719fe67f45a846111fccc4", + "zh:7473b036e9f8167c7a09e4865de95d07922eae6a612b94e819d44c87ba5298de", + "zh:783c73e66bf50a74983803e1ec6d6237bae2891f9d4fd4824cfd5350121552ae", + "zh:83681e1d8d002048b76d7144cb96c8c8501dc973d1fee41b7579a46ad9eb04c2", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b08b4168d4e2a81badbbe65d95f692ed3292c2b71cd00b9889a3bb7cf54c1188", + "zh:b4320ca25f4f67beebcbd6563ece2e490bd9dcb0a7e59b5d7a437ddaf53768ed", + "zh:d7f99254d6e05bac3dffae9b437c47cd807b4e66d941e56364be0a28ead418bd", + "zh:e433e91689758a341c91840cc7b5d3a3c5089004766d20659d57199b88ad8a5f", + "zh:e4c5a9b0f96a5fe2b5ed5592d4c2ac33240cf5308bcb14e52d6f2e0eb183a014", + "zh:fc4b554ae98e40e3ab6878ec9501ba2b05213d30668ee357d48b207993ffbe03", + ] +} diff --git a/bootstrap/README.md b/bootstrap/README.md new file mode 100644 index 0000000..0825003 --- /dev/null +++ b/bootstrap/README.md @@ -0,0 +1,36 @@ +# bootstrap + +Creates the S3 bucket that holds every environment's Terraform state. Run once +per AWS account, ever. + + terraform init + terraform apply -var 'region=us-east-1' + +It prints the bucket name and the `init` line the prod environment needs. + +## Why this is a separate root + +A backend cannot store the state of its own creation, so something has to make +the bucket first. That something keeps its state here, on local disk, in +`terraform.tfstate` — which is gitignored. + +**Losing that file is not a problem.** The bucket has `prevent_destroy`, so +nothing is going to remove it by accident, and re-adopting it is one command: + + terraform import aws_s3_bucket.state lance-blue-tfstate- + +followed by `terraform plan` to confirm the rest of the configuration matches +what is actually there. + +## What it makes + +One bucket, with versioning (the recovery path for a corrupted state file), +SSE-S3 (KMS charges per request and buys nothing here), public access blocked, +a TLS-only bucket policy, and lifecycle rules that expire old versions after 90 +days and abort abandoned multipart uploads. + +**No DynamoDB table.** Terraform 1.10 added S3-native state locking via a lock +file object, and `dynamodb_table` is deprecated as of 1.11. Environments set +`use_lockfile = true` in their backend block instead — one fewer resource, one +fewer thing to pay for, and no chance of the table and the bucket drifting +apart. diff --git a/bootstrap/main.tf b/bootstrap/main.tf new file mode 100644 index 0000000..81b5c06 --- /dev/null +++ b/bootstrap/main.tf @@ -0,0 +1,146 @@ +# The S3 bucket that holds every environment's state. +# +# This root has no backend of its own: a bucket cannot store the state of its +# own creation. Its state file stays on local disk, gitignored, and losing it +# costs a `terraform import` rather than the bucket - see README.md. +# +# Run once per AWS account, ever. + +terraform { + required_version = ">= 1.11" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + } +} + +provider "aws" { + region = var.region + + default_tags { + tags = { + Project = "lance.blue" + ManagedBy = "terraform" + Repo = "infra" + Component = "tfstate" + } + } +} + +data "aws_caller_identity" "current" {} + +# The account id is in the name because S3 bucket names are globally unique and +# "lance-blue-tfstate" is exactly the kind of name someone else has already +# taken. It also makes a wrong-account apply obvious in the output. +resource "aws_s3_bucket" "state" { + bucket = "${var.name_prefix}-tfstate-${data.aws_caller_identity.current.account_id}" + + # Deleting this deletes every environment's state. Terraform will refuse + # rather than let a bad plan through; that refusal is the point. + lifecycle { + prevent_destroy = true + } +} + +# The actual safety net. State files are kilobytes, so keeping every version is +# effectively free, and a corrupt apply is recoverable by restoring an object +# version rather than by rebuilding the account. +resource "aws_s3_bucket_versioning" "state" { + bucket = aws_s3_bucket.state.id + + versioning_configuration { + status = "Enabled" + } +} + +# SSE-S3 rather than KMS: KMS charges per request and per key, and buys nothing +# here. State is sensitive because of what Terraform puts in it, not because of +# who can reach the bucket - which is nobody, per the public access block. +resource "aws_s3_bucket_server_side_encryption_configuration" "state" { + bucket = aws_s3_bucket.state.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + bucket_key_enabled = true + } +} + +resource "aws_s3_bucket_public_access_block" "state" { + bucket = aws_s3_bucket.state.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +# Versioning without expiry grows forever. 90 days is long enough that any +# recovery worth attempting has already been attempted. +resource "aws_s3_bucket_lifecycle_configuration" "state" { + bucket = aws_s3_bucket.state.id + + rule { + id = "expire-noncurrent-state" + status = "Enabled" + + filter {} + + noncurrent_version_expiration { + noncurrent_days = 90 + } + } + + # State locking with `use_lockfile` writes and deletes small objects. A failed + # multipart upload is unlikely at that size, but an abandoned one bills until + # something removes it. + rule { + id = "abort-incomplete-uploads" + status = "Enabled" + + filter {} + + abort_incomplete_multipart_upload { + days_after_initiation = 7 + } + } + + depends_on = [aws_s3_bucket_versioning.state] +} + +# TLS-only. Not because anything here talks plain HTTP - the AWS SDKs do not - +# but because a bucket policy is the only place this can be stated as a rule +# rather than as an assumption. +resource "aws_s3_bucket_policy" "state" { + bucket = aws_s3_bucket.state.id + policy = data.aws_iam_policy_document.state.json +} + +data "aws_iam_policy_document" "state" { + statement { + sid = "DenyInsecureTransport" + effect = "Deny" + + principals { + type = "*" + identifiers = ["*"] + } + + actions = ["s3:*"] + + resources = [ + aws_s3_bucket.state.arn, + "${aws_s3_bucket.state.arn}/*", + ] + + condition { + test = "Bool" + variable = "aws:SecureTransport" + values = ["false"] + } + } +} diff --git a/bootstrap/outputs.tf b/bootstrap/outputs.tf new file mode 100644 index 0000000..4d5527d --- /dev/null +++ b/bootstrap/outputs.tf @@ -0,0 +1,9 @@ +output "state_bucket" { + description = "Name of the state bucket. This is the value envs/*/backend.tf needs." + value = aws_s3_bucket.state.id +} + +output "init_command" { + description = "Ready-made init for the prod environment, since a backend block cannot take variables." + value = "terraform -chdir=envs/prod init -backend-config=\"bucket=${aws_s3_bucket.state.id}\" -backend-config=\"region=${var.region}\"" +} diff --git a/bootstrap/variables.tf b/bootstrap/variables.tf new file mode 100644 index 0000000..c1883d1 --- /dev/null +++ b/bootstrap/variables.tf @@ -0,0 +1,11 @@ +variable "region" { + description = "AWS region for the state bucket. Should match the region the environments run in; the backend config in envs/*/backend.tf must agree with it." + type = string + default = "us-east-1" +} + +variable "name_prefix" { + description = "Prefix for resource names." + type = string + default = "lance-blue" +} diff --git a/envs/prod/.terraform.lock.hcl b/envs/prod/.terraform.lock.hcl new file mode 100644 index 0000000..3347a75 --- /dev/null +++ b/envs/prod/.terraform.lock.hcl @@ -0,0 +1,26 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.57.1" + constraints = "~> 6.0" + hashes = [ + "h1:WXndu9uKvbnmspexcbki89ZuGLt2SUyAfZ5GgQUm+QU=", + "zh:2d29e22480a81c21fb3f2fd52f9bd3ca4a82c37f3bb1b1036e881e42cddc75a1", + "zh:33aeb08e9973199b30f8a8e48a58dc67cfb6e32879f7a1c05c521899fe718f53", + "zh:37b7f977a7e7d45ad11d42958bc264873fb34573eee925915038ea05607abc9e", + "zh:41ebdcf4bcd073a01d58505a5f5118b85668de357d2d9f266926923e817a1842", + "zh:43093dfc3559c2c0467c92f48b29ae0221d52e912fce03dd77abd90806fdcc7d", + "zh:63b4252933e828d3590c0c64b827ec0f8955aa52df719fe67f45a846111fccc4", + "zh:7473b036e9f8167c7a09e4865de95d07922eae6a612b94e819d44c87ba5298de", + "zh:783c73e66bf50a74983803e1ec6d6237bae2891f9d4fd4824cfd5350121552ae", + "zh:83681e1d8d002048b76d7144cb96c8c8501dc973d1fee41b7579a46ad9eb04c2", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b08b4168d4e2a81badbbe65d95f692ed3292c2b71cd00b9889a3bb7cf54c1188", + "zh:b4320ca25f4f67beebcbd6563ece2e490bd9dcb0a7e59b5d7a437ddaf53768ed", + "zh:d7f99254d6e05bac3dffae9b437c47cd807b4e66d941e56364be0a28ead418bd", + "zh:e433e91689758a341c91840cc7b5d3a3c5089004766d20659d57199b88ad8a5f", + "zh:e4c5a9b0f96a5fe2b5ed5592d4c2ac33240cf5308bcb14e52d6f2e0eb183a014", + "zh:fc4b554ae98e40e3ab6878ec9501ba2b05213d30668ee357d48b207993ffbe03", + ] +} diff --git a/envs/prod/backend.tf b/envs/prod/backend.tf new file mode 100644 index 0000000..0f1ca25 --- /dev/null +++ b/envs/prod/backend.tf @@ -0,0 +1,23 @@ +# State lives in the bucket bootstrap/ creates. +# +# A backend block cannot use variables, so the bucket name is a placeholder that +# has to be either edited here or supplied at init: +# +# terraform -chdir=envs/prod init \ +# -backend-config="bucket=lance-blue-tfstate-" +# +# `bootstrap` prints the exact line as an output. +# +# No `dynamodb_table`: locking is the S3 lock file, which needs no second +# resource and is the supported mechanism from Terraform 1.11 on. + +terraform { + backend "s3" { + bucket = "REPLACE-ME-lance-blue-tfstate-" + key = "prod/terraform.tfstate" + region = "us-east-1" + + encrypt = true + use_lockfile = true + } +} diff --git a/envs/prod/main.tf b/envs/prod/main.tf new file mode 100644 index 0000000..7053ccb --- /dev/null +++ b/envs/prod/main.tf @@ -0,0 +1,23 @@ +# Production. The only environment - see docs/decisions.md#one-environment. +# +# This file composes modules and nothing else: no resources are declared here, +# so anything that turns out to be worth reusing already lives somewhere it can +# be reused from. + +locals { + # Prod carries the suffix like any other environment. The exception is + # anything in a global namespace that would be awkward to rename later, which + # names itself. + name_prefix = "lance-blue-${var.environment}" +} + +module "network" { + source = "../../modules/network" + + name_prefix = local.name_prefix + region = var.region + cidr_block = var.vpc_cidr + az_count = var.az_count + + tags = { Component = "network" } +} diff --git a/envs/prod/outputs.tf b/envs/prod/outputs.tf new file mode 100644 index 0000000..ce56c02 --- /dev/null +++ b/envs/prod/outputs.tf @@ -0,0 +1,9 @@ +output "vpc_id" { + description = "VPC id." + value = module.network.vpc_id +} + +output "public_subnet_ids" { + description = "Public subnets match tasks run in." + value = module.network.public_subnet_ids +} diff --git a/envs/prod/providers.tf b/envs/prod/providers.tf new file mode 100644 index 0000000..2d34b44 --- /dev/null +++ b/envs/prod/providers.tf @@ -0,0 +1,16 @@ +provider "aws" { + region = var.region + + # The cheapest possible defence against applying to the wrong account. + # Terraform refuses before it plans if the caller's account is not this one. + allowed_account_ids = [var.account_id] + + default_tags { + tags = { + Project = "lance.blue" + Environment = var.environment + ManagedBy = "terraform" + Repo = "infra" + } + } +} diff --git a/envs/prod/terraform.tfvars.example b/envs/prod/terraform.tfvars.example new file mode 100644 index 0000000..45afeb0 --- /dev/null +++ b/envs/prod/terraform.tfvars.example @@ -0,0 +1,8 @@ +# Copy to terraform.tfvars and fill in. That file is gitignored - the +# .gitignore treats every *.tfvars as suspect on principle, even when, as here, +# it holds nothing secret. + +account_id = "000000000000" +region = "us-east-1" + +domain_name = "lance.blue" diff --git a/envs/prod/variables.tf b/envs/prod/variables.tf new file mode 100644 index 0000000..f9dedda --- /dev/null +++ b/envs/prod/variables.tf @@ -0,0 +1,48 @@ +# Everything environment-specific lives here. Modules take inputs and know +# nothing about which account or region they are in, so a second environment is +# a directory copy rather than a refactor. +# +# Nothing in this file is secret. If a sensitive value ever becomes necessary, +# it is passed as -var or TF_VAR_* at the command line and never written to +# disk - and that is the moment to reconsider and reach for Secrets Manager +# instead. See docs/runbook.md. + +variable "account_id" { + description = "AWS account id. Checked by the provider before anything is planned, so a wrong-profile apply fails immediately." + type = string + + validation { + condition = can(regex("^[0-9]{12}$", var.account_id)) + error_message = "account_id must be 12 digits." + } +} + +variable "region" { + description = "AWS region. us-east-1 is cheapest and needs no aliased provider for CloudFront-facing certificates; it is a poor default for European players. See docs/decisions.md#region-us-east-1." + type = string + default = "us-east-1" +} + +variable "environment" { + description = "Environment name. Used in tags and in resource names." + type = string + default = "prod" +} + +variable "domain_name" { + description = "The apex domain. Player handles are subdomains of it." + type = string + default = "lance.blue" +} + +variable "vpc_cidr" { + description = "VPC CIDR." + type = string + default = "10.20.0.0/16" +} + +variable "az_count" { + description = "Availability zones to place public subnets in." + type = number + default = 3 +} diff --git a/envs/prod/versions.tf b/envs/prod/versions.tf new file mode 100644 index 0000000..d6fc022 --- /dev/null +++ b/envs/prod/versions.tf @@ -0,0 +1,13 @@ +terraform { + # 1.11 is the floor: S3-native state locking (`use_lockfile`) landed in 1.10 + # and `dynamodb_table` was deprecated in 1.11, so this is the first release + # where the backend below is the supported shape rather than a new option. + required_version = ">= 1.11" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + } +} diff --git a/modules/network/main.tf b/modules/network/main.tf new file mode 100644 index 0000000..755135d --- /dev/null +++ b/modules/network/main.tf @@ -0,0 +1,123 @@ +# The VPC match tasks run in. +# +# Public subnets only, no NAT gateway, no interface endpoints. A NAT gateway is +# ~$33/month before a byte moves and the three interface endpoints that would +# replace it are ~$21/month; a public IP on a task is $0.005/hour and only for +# the length of the match. See docs/decisions.md#no-nat-gateway. +# +# Public IP does not mean reachable. Nothing in this module opens ingress - +# security groups are owned by the modules that need them, and the match task's +# group allows none at all. The WebSocket proxy is the only thing that talks to +# a task. +# +# Private subnets are deliberately not created rather than created and left +# empty: an empty private subnet invites someone to put something in it and +# discover the egress problem later. + +terraform { + required_version = ">= 1.11" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + } +} + +data "aws_availability_zones" "available" { + state = "available" + + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +locals { + # Fargate capacity is per-AZ and occasionally short in one of them, so spread + # across a few. Subnets cost nothing; only what runs in them does. + azs = slice(data.aws_availability_zones.available.names, 0, var.az_count) + + # /20 per subnet out of a /16. Far more addresses than a match task needs, but + # the alternative is re-cutting the range the first time something else wants + # a subnet, and unused address space is free. + subnet_cidrs = [for i in range(var.az_count) : cidrsubnet(var.cidr_block, 4, i)] +} + +resource "aws_vpc" "this" { + cidr_block = var.cidr_block + + # DNS hostnames are required for the S3 gateway endpoint to be usable by name, + # and for anything that resolves an AWS service endpoint from inside the VPC. + enable_dns_support = true + enable_dns_hostnames = true + + tags = merge(var.tags, { Name = var.name_prefix }) +} + +resource "aws_internet_gateway" "this" { + vpc_id = aws_vpc.this.id + + tags = merge(var.tags, { Name = var.name_prefix }) +} + +resource "aws_subnet" "public" { + for_each = { for i, az in local.azs : az => local.subnet_cidrs[i] } + + vpc_id = aws_vpc.this.id + availability_zone = each.key + cidr_block = each.value + + # Tasks get a public IP because there is no NAT to reach the internet through. + # ECS also sets this per-task; both have to agree. + map_public_ip_on_launch = true + + tags = merge(var.tags, { Name = "${var.name_prefix}-public-${each.key}" }) +} + +# One route table for all public subnets - they are identical, and per-subnet +# tables would only be useful if their routes ever differed. +resource "aws_route_table" "public" { + vpc_id = aws_vpc.this.id + + tags = merge(var.tags, { Name = "${var.name_prefix}-public" }) +} + +resource "aws_route" "default" { + route_table_id = aws_route_table.public.id + destination_cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.this.id +} + +resource "aws_route_table_association" "public" { + for_each = aws_subnet.public + + subnet_id = each.value.id + route_table_id = aws_route_table.public.id +} + +# Gateway endpoints are free, unlike interface endpoints. This keeps S3 traffic +# - scenario downloads, camo, result uploads, and the layer pulls behind ECR - +# off the internet gateway path entirely. +# +# It is also the migration path if the assumption that in-region transfer over +# the IGW is free turns out to be wrong: S3 is already covered, and only ECR +# would need interface endpoints. +resource "aws_vpc_endpoint" "s3" { + vpc_id = aws_vpc.this.id + service_name = "com.amazonaws.${var.region}.s3" + vpc_endpoint_type = "Gateway" + route_table_ids = [aws_route_table.public.id] + + tags = merge(var.tags, { Name = "${var.name_prefix}-s3" }) +} + +# A VPC's default security group allows all traffic between anything that uses +# it. Nothing here should, so it is emptied - a default-open group that nothing +# references is still a default-open group waiting for someone to reference it. +resource "aws_default_security_group" "this" { + vpc_id = aws_vpc.this.id + + tags = merge(var.tags, { Name = "${var.name_prefix}-default-do-not-use" }) +} diff --git a/modules/network/outputs.tf b/modules/network/outputs.tf new file mode 100644 index 0000000..7d76f3a --- /dev/null +++ b/modules/network/outputs.tf @@ -0,0 +1,19 @@ +output "vpc_id" { + description = "VPC id." + value = aws_vpc.this.id +} + +output "vpc_cidr_block" { + description = "VPC CIDR, for security group rules that need to name the VPC's own range." + value = aws_vpc.this.cidr_block +} + +output "public_subnet_ids" { + description = "Public subnet ids, in availability-zone order. What an ECS network configuration wants." + value = [for az in local.azs : aws_subnet.public[az].id] +} + +output "availability_zones" { + description = "The availability zones subnets were placed in." + value = local.azs +} diff --git a/modules/network/variables.tf b/modules/network/variables.tf new file mode 100644 index 0000000..2a58921 --- /dev/null +++ b/modules/network/variables.tf @@ -0,0 +1,37 @@ +variable "name_prefix" { + description = "Prefix for resource names, e.g. lance-blue-prod." + type = string +} + +variable "region" { + description = "AWS region. Needed to build the S3 gateway endpoint service name; the provider's region is not readable from inside a module." + type = string +} + +variable "cidr_block" { + description = "VPC CIDR. Nothing peers with this VPC, so the range only has to avoid colliding with anything it might peer with later." + type = string + default = "10.20.0.0/16" + + validation { + condition = can(cidrnetmask(var.cidr_block)) + error_message = "cidr_block must be a valid IPv4 CIDR." + } +} + +variable "az_count" { + description = "How many availability zones to place public subnets in. Subnets are free; this only affects where Fargate can find capacity." + type = number + default = 3 + + validation { + condition = var.az_count >= 2 && var.az_count <= 4 + error_message = "az_count must be between 2 and 4." + } +} + +variable "tags" { + description = "Tags to merge into every resource, on top of the provider's default_tags." + type = map(string) + default = {} +} -- 2.51.2