Files
quixos/tofu/central/main.tf
T
2026-06-29 22:40:04 -07:00

485 lines
16 KiB
Terraform

data "aws_vpc" "default" {
count = var.vpc_id == null ? 1 : 0
default = true
}
data "aws_subnets" "default" {
count = var.subnet_id == null ? 1 : 0
filter {
name = "vpc-id"
values = [local.vpc_id]
}
}
data "aws_subnet" "selected" {
id = local.subnet_id
}
data "aws_ssm_parameter" "ami" {
count = var.ami_id == null && var.ami_ssm_parameter_name != null ? 1 : 0
name = var.ami_ssm_parameter_name
}
data "terraform_remote_state" "nix_cache" {
count = var.nix_cache_enable ? 1 : 0
backend = "s3"
config = {
bucket = var.nix_cache_state_bucket
key = var.nix_cache_state_key
region = var.nix_cache_state_region
}
}
data "external" "nix_cache_public_key" {
count = var.nix_cache_enable && var.nix_cache_public_key == null ? 1 : 0
program = ["${path.module}/read-optional-ssm.sh"]
query = {
name = data.terraform_remote_state.nix_cache[0].outputs.public_key_parameter_name
}
}
resource "random_id" "central" {
byte_length = 4
lifecycle {
precondition {
condition = !var.nix_cache_enable || var.nix_cache_bootstrap_optional || var.nix_cache_public_key != null || try(data.external.nix_cache_public_key[0].result.value != "", false)
error_message = "nix_cache_enable is true, but the nix-cache public key is not available. Apply tofu/nix-cache and wait for it to publish /quixos/nix-cache/public-key, set nix_cache_public_key explicitly, or set nix_cache_bootstrap_optional=true for the first bootstrap apply."
}
}
}
locals {
vpc_id = var.vpc_id == null ? data.aws_vpc.default[0].id : var.vpc_id
subnet_id = var.subnet_id == null ? data.aws_subnets.default[0].ids[0] : var.subnet_id
resource_name = "${var.name}-${random_id.central.hex}"
bucket_name = var.runtime_state_bucket_name == null ? "${local.resource_name}-runtime-state" : var.runtime_state_bucket_name
base_domain = trimsuffix(var.base_domain, ".")
app_server_domain = var.app_server_domain == null ? local.base_domain : trimsuffix(var.app_server_domain, ".")
app_wildcard_domain = "app.${local.base_domain}"
dev_app_wildcard_domain = "dev.app.${local.base_domain}"
central_ami_id = var.ami_id == null && var.ami_ssm_parameter_name != null ? data.aws_ssm_parameter.ami[0].value : var.ami_id
app_server_ami_id = var.app_server_ami_id == null ? local.central_ami_id : var.app_server_ami_id
app_server_enabled = var.app_server_ami_id != null || var.ami_id != null || var.ami_ssm_parameter_name != null
app_server_flake_uri = "git+${var.app_server_repo_url}?dir=services/app-server&${var.app_server_ref_is_rev ? "rev" : "ref"}=${urlencode(var.app_server_ref)}"
app_server_persist_device = "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_${replace(aws_ebs_volume.persist.id, "-", "")}"
nix_cache_outputs = var.nix_cache_enable ? data.terraform_remote_state.nix_cache[0].outputs : null
nix_cache_hosts_parts = var.nix_cache_enable ? split(" ", local.nix_cache_outputs.hosts_entry) : []
nix_cache_host_address = var.nix_cache_enable ? local.nix_cache_hosts_parts[0] : ""
nix_cache_domain = var.nix_cache_enable ? local.nix_cache_outputs.domain : ""
nix_cache_endpoint = var.nix_cache_enable ? "http://${local.nix_cache_outputs.domain}" : ""
nix_cache_substituter = var.nix_cache_enable ? local.nix_cache_outputs.cache_url : ""
nix_cache_cache_name = var.nix_cache_enable ? trimprefix(local.nix_cache_outputs.cache_url, "http://${local.nix_cache_outputs.domain}/") : ""
nix_cache_public_key = var.nix_cache_enable ? (var.nix_cache_public_key != null ? var.nix_cache_public_key : data.external.nix_cache_public_key[0].result.value) : ""
nix_cache_push_parameter = var.nix_cache_enable ? local.nix_cache_outputs.central_push_token_parameter_name : ""
nix_cache_ready = var.nix_cache_enable && local.nix_cache_host_address != "" && local.nix_cache_public_key != ""
platform_vpc_id = var.platform_vpc_enable ? aws_vpc.platform[0].id : local.vpc_id
platform_public_subnet_id = var.platform_vpc_enable ? aws_subnet.central_public[0].id : local.subnet_id
platform_runtime_subnet_id = var.platform_vpc_enable ? aws_subnet.runtime_public[0].id : local.subnet_id
tags = {
Name = local.resource_name
Project = var.name
Role = "central-infrastructure"
"quixos:managed-by" = "tofu/central"
"quixos:project" = var.name
}
}
resource "aws_vpc" "platform" {
count = var.platform_vpc_enable ? 1 : 0
cidr_block = var.platform_vpc_cidr
assign_generated_ipv6_cidr_block = true
enable_dns_hostnames = true
enable_dns_support = true
tags = merge(local.tags, {
Name = "${local.resource_name}-vpc"
Role = "platform-vpc"
})
}
resource "aws_internet_gateway" "platform" {
count = var.platform_vpc_enable ? 1 : 0
vpc_id = aws_vpc.platform[0].id
tags = merge(local.tags, {
Name = "${local.resource_name}-igw"
Role = "platform-internet-gateway"
})
}
resource "aws_subnet" "central_public" {
count = var.platform_vpc_enable ? 1 : 0
vpc_id = aws_vpc.platform[0].id
availability_zone = data.aws_subnet.selected.availability_zone
cidr_block = var.platform_public_subnet_cidr
ipv6_cidr_block = cidrsubnet(aws_vpc.platform[0].ipv6_cidr_block, 8, 0)
assign_ipv6_address_on_creation = true
map_public_ip_on_launch = true
tags = merge(local.tags, {
Name = "${local.resource_name}-central-public"
Role = "central-public-subnet"
})
}
resource "aws_subnet" "runtime_public" {
count = var.platform_vpc_enable ? 1 : 0
vpc_id = aws_vpc.platform[0].id
availability_zone = data.aws_subnet.selected.availability_zone
cidr_block = var.platform_runtime_subnet_cidr
ipv6_cidr_block = cidrsubnet(aws_vpc.platform[0].ipv6_cidr_block, 8, 1)
assign_ipv6_address_on_creation = true
map_public_ip_on_launch = true
tags = merge(local.tags, {
Name = "${local.resource_name}-runtime-public"
Role = "runtime-public-egress-subnet"
})
}
resource "aws_route_table" "platform_public" {
count = var.platform_vpc_enable ? 1 : 0
vpc_id = aws_vpc.platform[0].id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.platform[0].id
}
route {
ipv6_cidr_block = "::/0"
gateway_id = aws_internet_gateway.platform[0].id
}
tags = merge(local.tags, {
Name = "${local.resource_name}-public-rt"
Role = "platform-public-route-table"
})
}
resource "aws_route_table_association" "central_public" {
count = var.platform_vpc_enable ? 1 : 0
subnet_id = aws_subnet.central_public[0].id
route_table_id = aws_route_table.platform_public[0].id
}
resource "aws_route_table_association" "runtime_public" {
count = var.platform_vpc_enable ? 1 : 0
subnet_id = aws_subnet.runtime_public[0].id
route_table_id = aws_route_table.platform_public[0].id
}
resource "aws_s3_bucket" "runtime_state" {
bucket = local.bucket_name
force_destroy = var.runtime_state_bucket_force_destroy
tags = merge(local.tags, {
Name = local.bucket_name
Role = "runtime-deployment-state"
})
}
resource "aws_s3_bucket_public_access_block" "runtime_state" {
bucket = aws_s3_bucket.runtime_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "runtime_state" {
bucket = aws_s3_bucket.runtime_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_versioning" "runtime_state" {
bucket = aws_s3_bucket.runtime_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_ebs_volume" "persist" {
availability_zone = data.aws_subnet.selected.availability_zone
size = var.volume_size_gb
snapshot_id = var.snapshot_id
type = var.volume_type
encrypted = true
tags = merge(local.tags, {
Name = "${local.resource_name}-volume"
Role = "persistent-state"
})
}
resource "aws_security_group" "app_server" {
count = local.app_server_enabled ? 1 : 0
name = "${local.resource_name}-central-proxy"
description = "Quixos central app-server ingress"
vpc_id = local.platform_vpc_id
tags = merge(local.tags, {
Name = "${local.resource_name}-central-proxy-sg"
Role = "central-app-server-security-group"
})
}
resource "aws_vpc_security_group_ingress_rule" "app_server_http_ipv4" {
count = local.app_server_enabled ? 1 : 0
security_group_id = aws_security_group.app_server[0].id
description = "HTTP"
cidr_ipv4 = "0.0.0.0/0"
from_port = 80
ip_protocol = "tcp"
to_port = 80
}
resource "aws_vpc_security_group_ingress_rule" "app_server_http_ipv6" {
count = local.app_server_enabled ? 1 : 0
security_group_id = aws_security_group.app_server[0].id
description = "HTTP"
cidr_ipv6 = "::/0"
from_port = 80
ip_protocol = "tcp"
to_port = 80
}
resource "aws_vpc_security_group_ingress_rule" "app_server_https_ipv4" {
count = local.app_server_enabled ? 1 : 0
security_group_id = aws_security_group.app_server[0].id
description = "HTTPS"
cidr_ipv4 = "0.0.0.0/0"
from_port = 443
ip_protocol = "tcp"
to_port = 443
}
resource "aws_vpc_security_group_ingress_rule" "app_server_https_ipv6" {
count = local.app_server_enabled ? 1 : 0
security_group_id = aws_security_group.app_server[0].id
description = "HTTPS"
cidr_ipv6 = "::/0"
from_port = 443
ip_protocol = "tcp"
to_port = 443
}
resource "aws_vpc_security_group_egress_rule" "app_server_ipv4_all" {
count = local.app_server_enabled ? 1 : 0
security_group_id = aws_security_group.app_server[0].id
description = "Outbound IPv4"
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
}
resource "aws_vpc_security_group_egress_rule" "app_server_runtime_http_ipv6" {
count = local.app_server_enabled && var.platform_vpc_enable ? 1 : 0
security_group_id = aws_security_group.app_server[0].id
description = "Runtime HTTP over platform IPv6"
cidr_ipv6 = aws_vpc.platform[0].ipv6_cidr_block
from_port = 80
ip_protocol = "tcp"
to_port = 80
}
resource "aws_vpc_security_group_egress_rule" "app_server_runtime_https_ipv6" {
count = local.app_server_enabled && var.platform_vpc_enable ? 1 : 0
security_group_id = aws_security_group.app_server[0].id
description = "Runtime HTTPS over platform IPv6"
cidr_ipv6 = aws_vpc.platform[0].ipv6_cidr_block
from_port = 443
ip_protocol = "tcp"
to_port = 443
}
resource "tls_private_key" "proxy_grants" {
algorithm = "ECDSA"
ecdsa_curve = "P256"
}
resource "aws_iam_role" "app_server" {
count = local.app_server_enabled ? 1 : 0
name = "${local.resource_name}-app-server"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
tags = local.tags
}
resource "aws_iam_role_policy_attachment" "app_server_ssm" {
count = local.app_server_enabled ? 1 : 0
role = aws_iam_role.app_server[0].name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_role_policy" "app_server_route53" {
count = local.app_server_enabled && var.route53_zone_id != null ? 1 : 0
name = "${local.resource_name}-route53"
role = aws_iam_role.app_server[0].name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"route53:ChangeResourceRecordSets",
"route53:ListResourceRecordSets"
]
Resource = "arn:aws:route53:::hostedzone/${var.route53_zone_id}"
},
{
Effect = "Allow"
Action = [
"route53:GetChange",
"route53:ListHostedZonesByName"
]
Resource = "*"
}
]
})
}
resource "aws_iam_role_policy" "app_server_nix_cache" {
count = local.app_server_enabled && local.nix_cache_ready ? 1 : 0
name = "${local.resource_name}-nix-cache"
role = aws_iam_role.app_server[0].name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ssm:GetParameter"
]
Resource = "arn:aws:ssm:${var.aws_region}:*:parameter${local.nix_cache_push_parameter}"
},
{
Effect = "Allow"
Action = "kms:Decrypt"
Resource = "*"
}
]
})
}
resource "aws_iam_instance_profile" "app_server" {
count = local.app_server_enabled ? 1 : 0
name = "${local.resource_name}-app-server"
role = aws_iam_role.app_server[0].name
}
resource "aws_instance" "app_server" {
count = local.app_server_enabled ? 1 : 0
ami = local.app_server_ami_id
instance_type = var.app_server_instance_type
subnet_id = local.platform_public_subnet_id
vpc_security_group_ids = [aws_security_group.app_server[0].id]
iam_instance_profile = aws_iam_instance_profile.app_server[0].name
associate_public_ip_address = true
ipv6_address_count = var.platform_vpc_enable ? 1 : null
user_data_replace_on_change = true
user_data = templatefile("${path.module}/user-data-flake.nix.tftpl", {
app_server_flake_uri = local.app_server_flake_uri
app_wildcard_domain = local.app_wildcard_domain
dev_app_wildcard_domain = local.dev_app_wildcard_domain
domain = local.app_server_domain
persist_device = local.app_server_persist_device
nix_cache_enable = local.nix_cache_ready
nix_cache_domain = local.nix_cache_domain
nix_cache_host_address = local.nix_cache_host_address
nix_cache_endpoint = local.nix_cache_endpoint
nix_cache_substituter = local.nix_cache_substituter
nix_cache_public_key = local.nix_cache_public_key
nix_cache_cache_name = local.nix_cache_cache_name
nix_cache_push_parameter = local.nix_cache_push_parameter
proxy_grant_public_key_pem = jsonencode(tls_private_key.proxy_grants.public_key_pem)
route53_zone_id = var.route53_zone_id
system = var.app_server_system
})
root_block_device {
volume_size = var.app_server_root_volume_size_gb
volume_type = "gp3"
}
tags = merge(local.tags, {
Name = "${local.resource_name}-app-server"
Role = "central-app-server"
})
}
resource "aws_volume_attachment" "app_server_persist" {
count = local.app_server_enabled ? 1 : 0
device_name = "/dev/sdh"
volume_id = aws_ebs_volume.persist.id
instance_id = aws_instance.app_server[0].id
}
resource "aws_eip" "app_server" {
count = local.app_server_enabled ? 1 : 0
domain = "vpc"
tags = merge(local.tags, {
Name = "${local.resource_name}-app-server-eip"
Role = "central-app-server-public-ip"
})
}
resource "aws_eip_association" "app_server" {
count = local.app_server_enabled ? 1 : 0
instance_id = aws_instance.app_server[0].id
allocation_id = aws_eip.app_server[0].id
}
resource "aws_route53_record" "app_server" {
count = local.app_server_enabled && var.route53_zone_id != null ? 1 : 0
zone_id = var.route53_zone_id
name = local.app_server_domain
type = "A"
ttl = 60
records = [aws_eip.app_server[0].public_ip]
}
resource "aws_route53_record" "app_wildcard" {
count = local.app_server_enabled && var.route53_zone_id != null ? 1 : 0
zone_id = var.route53_zone_id
name = "*.${local.app_wildcard_domain}"
type = "A"
ttl = 60
records = [aws_eip.app_server[0].public_ip]
}
resource "aws_route53_record" "dev_app_wildcard" {
count = local.app_server_enabled && var.route53_zone_id != null ? 1 : 0
zone_id = var.route53_zone_id
name = "*.${local.dev_app_wildcard_domain}"
type = "A"
ttl = 60
records = [aws_eip.app_server[0].public_ip]
}