data "vkcs_images_image" "ubuntu24" { visibility = "public" most_recent = true properties = { mcs_os_distro = "ubuntu" mcs_os_version = "24.04" } } data "vkcs_networking_network" "extnet" { name = "internet" sdn = "sprut" } # Every VK Cloud project auto-creates a "default" security group with a # UUID unique to that project - resolve it dynamically instead of # hardcoding one project's UUID. var.default_security_group_id is a # last-resort manual override (e.g. non-standard name/SDN in a project). data "vkcs_networking_secgroup" "default" { name = "default" sdn = "sprut" } locals { default_security_group_id = coalesce( var.default_security_group_id, data.vkcs_networking_secgroup.default.id ) } # Nova keypairs are per-user, not per-project - a keypair uploaded under a # different account is invisible to whichever account Terraform deploys as. # Register it here from var.ssh_public_key when supplied; otherwise assume # var.ssh_key_name already exists under the deploying account. resource "vkcs_compute_keypair" "router" { count = var.ssh_public_key != null ? 1 : 0 name = var.ssh_key_name public_key = var.ssh_public_key } locals { ssh_key_name = var.ssh_public_key != null ? vkcs_compute_keypair.router[0].name : var.ssh_key_name } # Security Groups resource "vkcs_networking_secgroup" "router_sg" { name = "router-sg" sdn = "sprut" } # Router SG rules resource "vkcs_networking_secgroup_rule" "router_ssh" { direction = "ingress" protocol = "tcp" port_range_min = 22 port_range_max = 22 remote_ip_prefix = "0.0.0.0/0" security_group_id = vkcs_networking_secgroup.router_sg.id sdn = "sprut" } resource "vkcs_networking_secgroup_rule" "router_icmp" { direction = "ingress" protocol = "icmp" remote_ip_prefix = "0.0.0.0/0" security_group_id = vkcs_networking_secgroup.router_sg.id sdn = "sprut" } resource "vkcs_networking_secgroup_rule" "router_ipsec_ike" { direction = "ingress" protocol = "udp" port_range_min = 500 port_range_max = 500 remote_ip_prefix = "0.0.0.0/0" security_group_id = vkcs_networking_secgroup.router_sg.id sdn = "sprut" } resource "vkcs_networking_secgroup_rule" "router_ipsec_nat_t" { direction = "ingress" protocol = "udp" port_range_min = 4500 port_range_max = 4500 remote_ip_prefix = "0.0.0.0/0" security_group_id = vkcs_networking_secgroup.router_sg.id sdn = "sprut" } # Per-router private interfaces: one shared private network per admin-supplied # CIDR in private_network_cidrs (no VRRP), each router getting its own # port/IP inside every such network. locals { private_network_cidr = { for idx, cidr in var.private_network_cidrs : "priv${idx + 1}" => cidr } } # Unify both sources of per-router interfaces into one role -> definition map: # - roles from private_network_cidr are project-managed (network/subnet created # below), address chosen by Neutron IPAM (fixed_ips left null). # - roles from var.router_networks are pre-existing networks owned elsewhere, # referenced by UUID only, with a specific IP per router (fixed_ips set). locals { router_interfaces = merge( { for role, cidr in local.private_network_cidr : role => { network_id = vkcs_networking_network.router_priv_net[role].id subnet_id = vkcs_networking_subnet.router_priv_subnet[role].id cidr = cidr fixed_ips = null } }, { for role, net in var.router_networks : role => { network_id = net.network_id subnet_id = net.subnet_id cidr = net.cidr fixed_ips = net.ip_addresses } } ) router_interface_roles = keys(local.router_interfaces) } resource "vkcs_networking_network" "router_priv_net" { for_each = local.private_network_cidr name = "router-${each.key}-net" sdn = "sprut" admin_state_up = true } resource "vkcs_networking_subnet" "router_priv_subnet" { for_each = local.private_network_cidr network_id = vkcs_networking_network.router_priv_net[each.key].id name = "router-${each.key}-subnet" cidr = each.value gateway_ip = cidrhost(each.value, 1) sdn = "sprut" # No in-guest DHCP client is needed for these interfaces: with # config_drive = true (set on every router instance), cloud-init learns # each interface's assigned address from the config-drive metadata and # renders it directly, then network-init.sh.tpl re-pins it deterministically # to ethN by MAC - no actual DHCP protocol exchange with this subnet ever # happens. (Note: this does NOT avoid IP collisions with VKCS's own # auto-created service ports on the network, e.g. a "network:dns" port - # see router_iface_port below, which leaves ip_address unset for these # project-managed roles instead.) enable_dhcp = false } resource "vkcs_networking_port" "router_iface_port" { for_each = { for pair in setproduct(range(var.router_count), local.router_interface_roles) : "router${pair[0] + 1}-${pair[1]}" => pair } name = "router-${each.key}-port" network_id = local.router_interfaces[each.value[1]].network_id admin_state_up = true port_security_enabled = false full_security_groups_control = true security_group_ids = [] sdn = "sprut" # For project-managed roles (private_network_cidr), ip_address is left null # so Neutron's IPAM auto-assigns one from the subnet's pool: VKCS # auto-creates its own service ports on this network (observed: a # "network:dns" port) that can silently consume whichever low address a # hand-computed offset would have picked, causing IpAddressAlreadyAllocated # - IPAM guarantees no double-booking, our own arithmetic doesn't. # network-init.sh.tpl matches interfaces by which declared CIDR their live # IP falls into, not by an exact expected IP, so this works either way. # For var.router_networks roles, the address is pre-agreed externally # (another project's admin already carved it out), so it's set explicitly. fixed_ip { subnet_id = local.router_interfaces[each.value[1]].subnet_id ip_address = ( local.router_interfaces[each.value[1]].fixed_ips == null ? null : local.router_interfaces[each.value[1]].fixed_ips[each.value[0]] ) } lifecycle { precondition { condition = ( local.router_interfaces[each.value[1]].fixed_ips == null || each.value[0] < length(local.router_interfaces[each.value[1]].fixed_ips) ) error_message = "router_networks[\"${each.value[1]}\"].ip_addresses must have at least var.router_count entries." } } } resource "vkcs_compute_instance" "router" { count = var.router_count name = "router${count.index + 1}" flavor_name = "STD3-4-4" availability_zone = element(var.router_availability_zones, count.index % length(var.router_availability_zones)) key_pair = local.ssh_key_name security_group_ids = [ vkcs_networking_secgroup.router_sg.id, local.default_security_group_id ] config_drive = true # Configure persistent networking using script: WAN via heuristic public-IP # detection, private interfaces matched by their expected CIDR. user_data = templatefile("${path.module}/scripts/network-init.sh.tpl", { private_interfaces = [ for role in local.router_interface_roles : { name = "eth${index(local.router_interface_roles, role) + 1}" cidr = local.router_interfaces[role].cidr } ] }) # WAN: dynamically created port network { uuid = data.vkcs_networking_network.extnet.id } # Private: one pre-created port per role, into that role's network (either # project-managed via private_network_cidrs, or a pre-existing externally # owned one via router_networks) dynamic "network" { for_each = local.router_interface_roles content { port = vkcs_networking_port.router_iface_port["router${count.index + 1}-${network.value}"].id } } block_device { uuid = data.vkcs_images_image.ubuntu24.id source_type = "image" volume_size = 20 boot_index = 0 destination_type = "volume" volume_type = "ceph-ssd" delete_on_termination = true } }