Files
CloudRouterAdvanced/terraform/main.tf
T
ayurishchevandClaude Sonnet 5 b2d87c19d8 Add router_networks: fixed-IP router interfaces into external networks (mvm-s3)
mvm-s3 is a separate VK Cloud project whose admin pre-created two private
networks/subnets with a known IP per router. Unify project-managed
(private_network_cidrs, IPAM-assigned) and externally-owned (router_networks,
fixed-IP) private interfaces into one local.router_interfaces so both share
the existing port/dynamic-network mechanism instead of duplicating it.

Switch from implicit *.auto.tfvars loading to explicit -var-file per
environment (now two share this terraform/ directory) plus a dedicated
Terraform workspace for mvm-s3, so PROD's state and credentials are never
touched by mvm-s3 applies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHfG9FgpMrGdrvC1QUewTw
2026-09-09 22:09:19 +03:00

249 lines
8.3 KiB
Terraform

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
}
}