From b11172504d12fe9e6f1b5b85c599152289e212f6 Mon Sep 17 00:00:00 2001 From: Alexandros Kritikos Date: Sun, 1 Mar 2026 14:16:44 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20initial=20Terraform=20configu?= =?UTF-8?q?ration=20for=20Azure=20and=20Proxmox=20resources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .terraform.lock.hcl | 99 +++++++++++++++++++++++++++++ backend.hcl | 5 ++ main.tf | 29 +++++++++ modules/dns/main.tf | 76 ++++++++++++++++++++++ modules/dns/variables.tf | 21 ++++++ modules/foundry/main.tf | 78 +++++++++++++++++++++++ modules/foundry/outputs.tf | 14 ++++ modules/foundry/variables.tf | 116 ++++++++++++++++++++++++++++++++++ modules/pangolin/firewall.tf | 100 +++++++++++++++++++++++++++++ modules/pangolin/main.tf | 116 ++++++++++++++++++++++++++++++++++ modules/pangolin/outputs.tf | 11 ++++ modules/pangolin/variables.tf | 68 ++++++++++++++++++++ modules/pip/main.tf | 20 ++++++ modules/pip/outputs.tf | 4 ++ modules/pip/variables.tf | 27 ++++++++ terraform.tf | 39 ++++++++++++ variables.tf | 46 ++++++++++++++ 17 files changed, 869 insertions(+) create mode 100644 .terraform.lock.hcl create mode 100644 backend.hcl create mode 100644 main.tf create mode 100644 modules/dns/main.tf create mode 100644 modules/dns/variables.tf create mode 100644 modules/foundry/main.tf create mode 100644 modules/foundry/outputs.tf create mode 100644 modules/foundry/variables.tf create mode 100644 modules/pangolin/firewall.tf create mode 100644 modules/pangolin/main.tf create mode 100644 modules/pangolin/outputs.tf create mode 100644 modules/pangolin/variables.tf create mode 100644 modules/pip/main.tf create mode 100644 modules/pip/outputs.tf create mode 100644 modules/pip/variables.tf create mode 100644 terraform.tf create mode 100644 variables.tf diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl new file mode 100644 index 0000000..5d1dc2b --- /dev/null +++ b/.terraform.lock.hcl @@ -0,0 +1,99 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/bpg/proxmox" { + version = "0.89.1" + constraints = ">= 0.89.1" + hashes = [ + "h1:ejdXiDjCc+YT1UKGIiCfrKbuMXUf1A9m3uIGMLIxoTI=", + "zh:0e0fda738ad915e4b9fa27df3f16e909007f71e907e2fce9d115bfadfc90f2fc", + "zh:1f07aecfc9f30f9124bb8a6ae6bc905ce6a069548d95088cdbc2a2b40a44088c", + "zh:1ff654f415b007e3967cde841ec455d331f45233866e551a668266ed901d7c5b", + "zh:2c03852beadc8a33974b6231cdde6eadb4dbd0383d57df04ddfbc130e58b4269", + "zh:3bef5fb1e4599d60845e87a0e8951bac1ffabe936f5c0b510bb419f755740544", + "zh:3dffa7a617f78fb30a70b88b08c68fe8729a5bc05c864555ad1d1dab17dc42cb", + "zh:41a810bef96ed529a919a8f93119d3ecba46f8cf4c48344ae0c710453425bc22", + "zh:4781ea719b841725d1bbea66a5cb209efadddb0d3ed59ddb91a3ea21c059f880", + "zh:7b6fcc76c86c6c282b61629aa8806c2f3ace4685853437484f6ae4b50422aafe", + "zh:8138970919956cc961e91038786f5c815948de950c43aef9b18b1aab280234e0", + "zh:82158d9dd32f67ca5f2a424d555e66cae14d2da25bc4316c71e1e4fd166f660c", + "zh:b961f3778d35c8169994bb369c9506dad62655782951e8644641062ebde63ed9", + "zh:be6acbfe0919fa2aba902349aadda7a0f545320856902dfa6fff54c3ae3a1a9b", + "zh:c06e6a5d41bc24558d77fbb4ac08399ce732ce722111c99c7c4d63211f36f08a", + "zh:f26e0763dbe6a6b2195c94b44696f2110f7f55433dc142839be16b9697fa5597", + ] +} + +provider "registry.opentofu.org/cloudflare/cloudflare" { + version = "5.15.0" + constraints = ">= 5.14.0" + hashes = [ + "h1:ZpfZzvKX16IgFRhutTTy/pCqw1Yo4ms5XJB+EFAjGvA=", + "zh:20a72bdbb28435f11d165b367732369e8f8163100a214e89ad720dae03fafa0c", + "zh:2eabd7a51fd7aafcab9861631d85c895914857e4fcd6fe2dd80bac22e74a1f47", + "zh:62828afbc1ba0e0a64bbb7d5d42ae3c2fbbaabb793010b07eba770ba91bae94f", + "zh:6693f1021e52c34a629300fbcd91f8bd4ca386fda3b45aec746b9c200c28a42c", + "zh:6873a15454b289e5baecc1d36ce8997266438761386a320753c63f13407f4a6b", + "zh:afbf4e56b3a5e5950b35b02b553313e4a2008415920b23f536682269c64ca549", + "zh:db367612900bc2e5a01c6a325e4cff9b1b04960ce9de3dd41671dda5a627ca1d", + "zh:eb7365eafc6160c3b304a9ce6a598e5400a2e779e9e2bd27976df244f79f774f", + "zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32", + ] +} + +provider "registry.opentofu.org/hashicorp/azurerm" { + version = "4.57.0" + constraints = ">= 4.56.0" + hashes = [ + "h1:KED8gvQ5w91ZBKSTp5fM7GJHWezd/2rrZB4/kGTn5hE=", + "zh:180d2585d2f5f88c6a2308ea3f8b2749e16a1d0c3fe91f153743534d2b672dd5", + "zh:1cd5beca4e0c4c32d3a75cd8db9491e6b64367e04dace4569ae7cd5414cac15d", + "zh:5508c45012c480fabf085f4f3b7a5bc904bb968f524484b3e75f7940ede12dd5", + "zh:64438b231bde02e7a282f525405e84a73e69dd4b32196ed1aa64fc9078dfbdd8", + "zh:7adc305c203d9816867e4b4527f773c002811f50807ed845a9c85db8d6b52d7b", + "zh:7d151cc53c22bfb24471c590f0385c51dc1f72b3db6e54dc32e0491b2223fd20", + "zh:d7b8d59f82903ac393aac1b118d82d47623acb665e5a771cb44aa50550eec3f2", + "zh:e053428171b1dd11a33ebbf634231cf06a2023b165ee24643c3705dcc4c5288e", + "zh:f89a738ecc88e2ce621e2beaef74afdb73ded22a027432f28f58b7f4b2541138", + ] +} + +provider "registry.opentofu.org/hashicorp/http" { + version = "3.5.0" + hashes = [ + "h1:bSHOZnfmhragoS/ZaMQEffXMipITkKF6jhcmpxAWh9c=", + "zh:0a2b33494eec6a91a183629cf217e073be063624c5d3f70870456ddb478308e9", + "zh:180f40124fa01b98b3d2f79128646b151818e09d6a1a9ca08e0b032a0b1e9cb1", + "zh:3e29e1de149dc10bf78620526c7cb8c62cd76087f5630dfaba0e93cda1f3aa7b", + "zh:4420950200cf86042ec940d0e2c9b7c89966bf556bf8038ba36217eae663bca5", + "zh:5d1f7d02109b2e2dca7ec626e5563ee765583792d0fd64081286f16f9433bd0d", + "zh:8500b138d338b1994c4206aa577b5c44e1d7260825babcf43245a7075bfa52a5", + "zh:b42165a6c4cfb22825938272d12b676e4a6946ac4e750f85df870c947685df2d", + "zh:b919bf3ee8e3b01051a0da3433b443a925e272893d3724ee8fc0f666ec7012c9", + "zh:d13b81ea6755cae785b3e11634936cdff2dc1ec009dc9610d8e3c7eb32f42e69", + "zh:f1c9d2eb1a6b618ae77ad86649679241bd8d6aacec06d0a68d86f748687f4eb3", + ] +} + +provider "registry.opentofu.org/rerichardjr/ipify" { + version = "1.0.0" + constraints = ">= 1.0.0" + hashes = [ + "h1:T0XX1XUy6006PFuC2suq5wdPtHEKXseRHN0ujjNBVQc=", + "zh:2682418e37ef78cce7412b3efa8e443d2e21c5614578cfd88859fafa78f0bb4f", + "zh:367338bd669b99b9f5891b533c6138774a1621619b111a9c90bf246af69f9898", + "zh:4457a0a5523c8b40a7a77084452ac36ac81ba2ac48fee0f534ad2ea8e2687cc3", + "zh:457656974b03523add389df7738c70aeb4239a0899e22882a171d5356584b21e", + "zh:47f7861f5fd2034897caf5ef1b685d44ca3f9871c3df984070a4e27907b6cd33", + "zh:4e1e4707f4894ef57f86a5af9ba31747b9ebb1a1e49a76d8ea2c62d94b6c489f", + "zh:773b62012494fe315206496148e6577732b846b0a30484c938387dfa1edc352f", + "zh:ab9a007efb00775a67f6b21da4367b7cd19cc43ec7eab08fa5db135fd05c199d", + "zh:b34df21f4fef4529c0a5c7c1633fb324fc7e9cf73f61235162f0c9ac3a04ea2c", + "zh:c863039f578160a0be60f29291b6516b6952d9f45b5249b44309e23f21a04f62", + "zh:cb8b3c597cd1b87852bca40131294de63335aafee4b3da823f83a81f512fb11a", + "zh:cc665a76dec466ff49b2c9f72123e681d50be00977eee477cf2c251674bc0ebf", + "zh:d6c92d1b42454e03f0db7c82a7ae426dd3ff7758630916886003a28daa598001", + "zh:d95d3b41ccfd0ba4490432ec92d7fdcd8ca57a0b31ba12123ee82a2a7d2669a7", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/backend.hcl b/backend.hcl new file mode 100644 index 0000000..6d0722a --- /dev/null +++ b/backend.hcl @@ -0,0 +1,5 @@ +resource_group_name = "rg-tofu-state" +storage_account_name = "tofu239746state" +container_name = "tfstate" +key = "terraform.tfstate" +use_azuread_auth = true diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..9ddd99f --- /dev/null +++ b/main.tf @@ -0,0 +1,29 @@ +module "pip" { + source = "./modules/pip" +} + +module "pangolin" { + source = "./modules/pangolin" + ssh_pubkey = file(var.ssh_pubkey_path) + allowed_ssh_cidrs_ipv4 = ["${module.pip.ip}/32"] + admin_username = var.admin_username +} + +module "foundry" { + source = "./modules/foundry" + node_name = var.node_name + datastore_id = var.datastore_id + container_id = 200 + bridge = var.bridge + vlan_tag = 32 + ssh_pubkey = file(var.ssh_pubkey_path) +} + +module "dns" { + source = "./modules/dns" + domain_zone_id = var.domain_zone_id + domain_name = var.domain + pangolin-proxy-v4 = module.pangolin.public_ipv4 + pangolin-proxy-v6 = module.pangolin.public_ipv6 + cdn_subdomains = ["foundry"] +} diff --git a/modules/dns/main.tf b/modules/dns/main.tf new file mode 100644 index 0000000..5d9664a --- /dev/null +++ b/modules/dns/main.tf @@ -0,0 +1,76 @@ +terraform { + required_providers { + cloudflare = { + source = "cloudflare/cloudflare" + } + } +} + +resource "cloudflare_dns_record" "proxy_ipv4" { + zone_id = var.domain_zone_id + name = "${var.domain_name}" + content = var.pangolin-proxy-v4 + comment = "Azure VPS" + type = "A" + proxied = false + ttl = 1 +} + +resource "cloudflare_dns_record" "proxy_ipv6" { + zone_id = var.domain_zone_id + name = "${var.domain_name}" + content = var.pangolin-proxy-v6 + comment = "Azure VPS" + type = "AAAA" + proxied = false + ttl = 1 +} + + +resource "cloudflare_dns_record" "subdomains_ipv4" { + zone_id = var.domain_zone_id + name = "*.${var.domain_name}" + content = var.pangolin-proxy-v4 + comment = "Azure VPS" + type = "A" + proxied = false + ttl = 1 +} + +resource "cloudflare_dns_record" "subdomains_ipv6" { + zone_id = var.domain_zone_id + name = "*.${var.domain_name}" + content = var.pangolin-proxy-v6 + comment = "Azure VPS" + type = "AAAA" + proxied = false + ttl = 1 +} + +# ── CDN-proxied subdomains ─────────────────────────────────── +# Specific records with proxied=true override the wildcard for +# these subdomains, enabling Cloudflare edge caching. + +resource "cloudflare_dns_record" "cdn_ipv4" { + for_each = toset(var.cdn_subdomains) + + zone_id = var.domain_zone_id + name = "${each.value}.${var.domain_name}" + content = var.pangolin-proxy-v4 + comment = "CDN-proxied via Cloudflare" + type = "A" + proxied = true + ttl = 1 +} + +resource "cloudflare_dns_record" "cdn_ipv6" { + for_each = toset(var.cdn_subdomains) + + zone_id = var.domain_zone_id + name = "${each.value}.${var.domain_name}" + content = var.pangolin-proxy-v6 + comment = "CDN-proxied via Cloudflare" + type = "AAAA" + proxied = true + ttl = 1 +} diff --git a/modules/dns/variables.tf b/modules/dns/variables.tf new file mode 100644 index 0000000..671dd4c --- /dev/null +++ b/modules/dns/variables.tf @@ -0,0 +1,21 @@ +variable "domain_zone_id" { + type = string +} + +variable "domain_name" { + type = string +} + +variable "pangolin-proxy-v4" { + type = string +} + +variable "pangolin-proxy-v6" { + type = string +} + +variable "cdn_subdomains" { + description = "Subdomains to serve through Cloudflare proxy (CDN). These get proxied A/AAAA records that override the wildcard." + type = list(string) + default = [] +} diff --git a/modules/foundry/main.tf b/modules/foundry/main.tf new file mode 100644 index 0000000..a2fddd7 --- /dev/null +++ b/modules/foundry/main.tf @@ -0,0 +1,78 @@ +terraform { + required_providers { + proxmox = { + source = "bpg/proxmox" + } + } +} + +resource "proxmox_virtual_environment_container" "foundry" { + node_name = var.node_name + vm_id = var.container_id > 0 ? var.container_id : null + + description = "Foundry VTT - managed by OpenTofu" + tags = var.tags + + unprivileged = var.unprivileged + start_on_boot = var.start_on_boot + started = var.start_on_create + + # ── OS Template ──────────────────────────────────────────── + operating_system { + template_file_id = var.template + type = "ubuntu" + } + + # ── Features ─────────────────────────────────────────────── + # nesting is required for systemd >= 257 (Ubuntu 25.04+) + features { + nesting = true + } + + # ── Resources ────────────────────────────────────────────── + cpu { + cores = var.cores + } + + memory { + dedicated = var.memory + swap = var.swap + } + + disk { + datastore_id = var.datastore_id + size = var.disk_size + } + + # ── Networking ───────────────────────────────────────────── + network_interface { + name = "eth0" + bridge = var.bridge + vlan_id = var.vlan_tag + } + + initialization { + hostname = var.hostname + + ip_config { + ipv4 { + address = var.ip_address + gateway = var.gateway != "" ? var.gateway : null + } + } + + dns { + domain = var.dns_domain + servers = [var.dns_server] + } + + user_account { + keys = [var.ssh_pubkey] + } + } + + # Ignore template changes so we don't recreate on minor template updates + lifecycle { + ignore_changes = [operating_system] + } +} diff --git a/modules/foundry/outputs.tf b/modules/foundry/outputs.tf new file mode 100644 index 0000000..7a9078c --- /dev/null +++ b/modules/foundry/outputs.tf @@ -0,0 +1,14 @@ +output "container_id" { + description = "The VMID of the Foundry LXC container." + value = proxmox_virtual_environment_container.foundry.vm_id +} + +output "hostname" { + description = "The hostname of the container." + value = var.hostname +} + +output "ip_address" { + description = "The configured IP address (or 'dhcp')." + value = var.ip_address +} diff --git a/modules/foundry/variables.tf b/modules/foundry/variables.tf new file mode 100644 index 0000000..f87a742 --- /dev/null +++ b/modules/foundry/variables.tf @@ -0,0 +1,116 @@ +variable "node_name" { + description = "Proxmox node to create the container on." + type = string +} + +variable "datastore_id" { + description = "Proxmox datastore for the container root filesystem." + type = string +} + +variable "bridge" { + description = "Network bridge for the container NIC." + type = string + default = "vmbr0" +} + +variable "vlan_tag" { + description = "VLAN tag for the container NIC. null = untagged." + type = number + default = null +} + +variable "container_id" { + description = "VMID to assign to the LXC container. 0 = auto-assign." + type = number + default = 0 +} + +variable "hostname" { + description = "Hostname for the container." + type = string + default = "foundry" +} + +variable "cores" { + description = "Number of CPU cores." + type = number + default = 2 +} + +variable "memory" { + description = "Memory in MB." + type = number + default = 2048 +} + +variable "swap" { + description = "Swap in MB." + type = number + default = 512 +} + +variable "disk_size" { + description = "Root filesystem size in GB." + type = number + default = 16 +} + +variable "ssh_pubkey" { + description = "SSH public key for root access." + type = string +} + +variable "ip_address" { + description = "Static IPv4 address in CIDR notation, or 'dhcp'." + type = string + default = "dhcp" +} + +variable "gateway" { + description = "Default gateway IPv4. Leave empty when using DHCP." + type = string + default = "" +} + +variable "dns_domain" { + description = "DNS search domain." + type = string + default = "ad.kritikos.io" +} + +variable "dns_server" { + description = "DNS server address." + type = string + default = "10.10.10.1" +} + +variable "template" { + description = "LXC template to use (download or local path)." + type = string + default = "persephone:vztmpl/ubuntu-25.04-standard_25.04-1.1_amd64.tar.zst" +} + +variable "start_on_create" { + description = "Start the container immediately after creation." + type = bool + default = true +} + +variable "start_on_boot" { + description = "Start the container when the Proxmox node boots." + type = bool + default = true +} + +variable "tags" { + description = "Tags to apply to the container." + type = list(string) + default = ["foundry", "managed-by-tofu"] +} + +variable "unprivileged" { + description = "Run as unprivileged container (recommended)." + type = bool + default = true +} diff --git a/modules/pangolin/firewall.tf b/modules/pangolin/firewall.tf new file mode 100644 index 0000000..0c16000 --- /dev/null +++ b/modules/pangolin/firewall.tf @@ -0,0 +1,100 @@ +resource "azurerm_network_security_group" "nsg" { + name = "${var.vm_name}-nsg" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + tags = var.tags +} + +resource "azurerm_network_interface_security_group_association" "nic_nsg" { + network_interface_id = azurerm_network_interface.nic.id + network_security_group_id = azurerm_network_security_group.nsg.id +} + +resource "azurerm_network_security_rule" "allow_udp_51820" { + name = "Allow-Wireguard" + priority = 100 + direction = "Inbound" + access = "Allow" + protocol = "Udp" + source_port_range = "*" + destination_port_range = "51820" + source_address_prefix = "*" + destination_address_prefix = "*" + resource_group_name = azurerm_resource_group.rg.name + network_security_group_name = azurerm_network_security_group.nsg.name +} + + +resource "azurerm_network_security_rule" "allow_ssh_vps" { + name = "Allow-SSH-VPS" + priority = 110 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "2222" + destination_address_prefix = "*" + resource_group_name = azurerm_resource_group.rg.name + network_security_group_name = azurerm_network_security_group.nsg.name + + source_address_prefix = length(var.allowed_ssh_cidrs_ipv4) == 0 ? "*" : null + source_address_prefixes = length(var.allowed_ssh_cidrs_ipv4) > 0 ? var.allowed_ssh_cidrs_ipv4 : null +} + +resource "azurerm_network_security_rule" "allow_ssh_proxy" { + name = "Allow-SSH-Proxy" + priority = 120 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "22" + destination_address_prefix = "*" + resource_group_name = azurerm_resource_group.rg.name + network_security_group_name = azurerm_network_security_group.nsg.name + + source_address_prefix = "*" + source_address_prefixes = null +} + +resource "azurerm_network_security_rule" "allow_postgres" { + name = "Allow-Postgres" + priority = 130 + direction = "Inbound" + access = "Deny" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "5432" + source_address_prefix = "*" + destination_address_prefix = "*" + resource_group_name = azurerm_resource_group.rg.name + network_security_group_name = azurerm_network_security_group.nsg.name +} + +resource "azurerm_network_security_rule" "allow_http" { + name = "Allow-HTTP" + priority = 140 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "80" + source_address_prefix = "*" + destination_address_prefix = "*" + resource_group_name = azurerm_resource_group.rg.name + network_security_group_name = azurerm_network_security_group.nsg.name +} + +resource "azurerm_network_security_rule" "allow_https" { + name = "Allow-HTTPS" + priority = 150 + direction = "Inbound" + access = "Allow" + protocol = "*" + source_port_range = "*" + destination_port_range = "443" + source_address_prefix = "*" + destination_address_prefix = "*" + resource_group_name = azurerm_resource_group.rg.name + network_security_group_name = azurerm_network_security_group.nsg.name +} diff --git a/modules/pangolin/main.tf b/modules/pangolin/main.tf new file mode 100644 index 0000000..92ec82f --- /dev/null +++ b/modules/pangolin/main.tf @@ -0,0 +1,116 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + } + } +} + +resource "azurerm_resource_group" "rg" { + location = var.location + name = "rg-pangolin-${var.environment}-${var.location}-${var.instance}" +} + +resource "azurerm_linux_virtual_machine" "vm" { + name = var.vm_name + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + size = var.vm_size + + admin_username = var.admin_username + disable_password_authentication = true + + admin_ssh_key { + username = var.admin_username + public_key = var.ssh_pubkey + } + + os_disk { + name = "${var.vm_name}-osdisk" + caching = "ReadWrite" + storage_account_type = "Standard_LRS" + } + + source_image_reference { + publisher = "Canonical" + offer = "ubuntu-24_04-lts" + sku = "server-gen1" + version = "latest" + } + + network_interface_ids = [azurerm_network_interface.nic.id] + + tags = var.tags +} + +resource "azurerm_virtual_network" "vnet" { +name = "${var.vm_name}-vnet" +location = azurerm_resource_group.rg.location +resource_group_name = azurerm_resource_group.rg.name + + +address_space = [var.vnet_cidr_ipv4, var.vnet_cidr_ipv6] + + +tags = var.tags +} + + +resource "azurerm_subnet" "subnet" { +name = "${var.vm_name}-subnet" +resource_group_name = azurerm_resource_group.rg.name +virtual_network_name = azurerm_virtual_network.vnet.name + + +address_prefixes = [var.subnet_cidr_ipv4, var.subnet_cidr_ipv6] +} + +resource "azurerm_network_interface" "nic" { + name = "${var.vm_name}-nic" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + tags = var.tags + + ip_forwarding_enabled = true + + ip_configuration { + name = "ipconfig-v4" + subnet_id = azurerm_subnet.subnet.id + private_ip_address_allocation = "Dynamic" + private_ip_address_version = "IPv4" + public_ip_address_id = azurerm_public_ip.pip_v4.id + primary = true + } + + ip_configuration { + name = "ipconfig-v6" + subnet_id = azurerm_subnet.subnet.id + private_ip_address_allocation = "Dynamic" + private_ip_address_version = "IPv6" + public_ip_address_id = azurerm_public_ip.pip_v6.id + } +} + + + +resource "azurerm_public_ip" "pip_v4" { + name = "pip-pangolin-${var.environment}-${var.location}-${var.instance}-v4" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + + allocation_method = "Static" + sku = "Standard" + ip_version = "IPv4" + tags = var.tags +} + +resource "azurerm_public_ip" "pip_v6" { + name = "pip-pangolin-${var.environment}-${var.location}-${var.instance}-v6" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + + allocation_method = "Static" + sku = "Standard" + ip_version = "IPv6" + tags = var.tags +} diff --git a/modules/pangolin/outputs.tf b/modules/pangolin/outputs.tf new file mode 100644 index 0000000..a97372b --- /dev/null +++ b/modules/pangolin/outputs.tf @@ -0,0 +1,11 @@ +output "public_ipv4" { + value = azurerm_public_ip.pip_v4.ip_address +} + +output "public_ipv6" { + value = azurerm_public_ip.pip_v6.ip_address +} + +output "ssh_ipv4" { + value = "ssh ${var.admin_username}@${azurerm_public_ip.pip_v4.ip_address}" +} diff --git a/modules/pangolin/variables.tf b/modules/pangolin/variables.tf new file mode 100644 index 0000000..a423d83 --- /dev/null +++ b/modules/pangolin/variables.tf @@ -0,0 +1,68 @@ +variable "location" { + type = string + default = "westeurope" +} + +variable "environment" { + type = string + default = "prod" +} + +variable "instance" { + type = string + default = "homelab" +} + +variable "tags" { + type = map(string) + default = { + project = "pangolin" + env = "prod" + } +} + +variable "vm_name" { + type = string + default = "pangolin-proxy" +} + +variable "vm_size" { + type = string + default = "Standard_A2_v2" +} + +variable "admin_username" { + type = string + default = "azureuser" +} + +variable "ssh_pubkey" { + type = string +} + +variable "vnet_cidr_ipv4" { + type = string + default = "10.50.0.0/16" +} + +variable "vnet_cidr_ipv6" { + type = string + default = "fd7d:bb99:1da4::/48" +} + +variable "subnet_cidr_ipv4" { + type = string + default = "10.50.1.0/24" +} + + +variable "subnet_cidr_ipv6" { + type = string + default = "fd7d:bb99:1da4:195::/64" +} + +variable "allowed_ssh_cidrs_ipv4" { + type = list(string) + description = "IPv4 CIDRs allowed to SSH (22/tcp). Empty list means allow from anywhere." + default = [] +} diff --git a/modules/pip/main.tf b/modules/pip/main.tf new file mode 100644 index 0000000..d46697e --- /dev/null +++ b/modules/pip/main.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + http = { } + } +} + +data "http" "echoip" { + url = var.http_url + request_headers = var.http_request_headers + method = var.http_method + + insecure = core::startswith(var.http_url, "http://") + + lifecycle { + postcondition { + condition = contains([200,201,204], self.status_code) + error_message = "Status code ${self.status_code} indicates request failure." + } + } +} diff --git a/modules/pip/outputs.tf b/modules/pip/outputs.tf new file mode 100644 index 0000000..5fd3c72 --- /dev/null +++ b/modules/pip/outputs.tf @@ -0,0 +1,4 @@ +output "ip" { + description = "The `ip` field of the echoip response." + value = trimspace(data.http.echoip.response_body) +} diff --git a/modules/pip/variables.tf b/modules/pip/variables.tf new file mode 100644 index 0000000..b875542 --- /dev/null +++ b/modules/pip/variables.tf @@ -0,0 +1,27 @@ +variable "http_url" { + description = "URL for echoip service to use." + type = string + + default = "https://checkip.amazonaws.com" + + validation { + condition = can(regex("https?://", var.http_url)) + error_message = "The `http_url` variable must start either http:// or https://" + } +} + +variable "http_request_headers" { + description = "HTTP headers to send with the request" + type = map(any) + + default = { + Accept = "application/json" + } +} + +variable "http_method" { + description = "HTTP method to use for the request" + type = string + + default = "GET" +} diff --git a/terraform.tf b/terraform.tf new file mode 100644 index 0000000..76ac256 --- /dev/null +++ b/terraform.tf @@ -0,0 +1,39 @@ +terraform { + backend "azurerm" {} + + required_version = ">=1.11.1" + + required_providers { + proxmox = { + source = "bpg/proxmox" + version = ">=0.89.1" + } + ipify = { + source = "rerichardjr/ipify" + version = ">= 1.0.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = ">=4.56.0" + } + cloudflare = { + source = "cloudflare/cloudflare" + version = ">=5.14.0" + } + } +} + +provider "proxmox" { + endpoint = var.pve_api_url + api_token = var.pve_token + insecure = false +} + +provider "azurerm" { + features { } + subscription_id = var.azure_subscription_id +} + +provider "cloudflare" { + api_token = var.cloudflare_api_token +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..f96c45a --- /dev/null +++ b/variables.tf @@ -0,0 +1,46 @@ +variable "domain" { + type = string +} +variable "domain_zone_id" { + type = string +} +variable "cloudflare_api_token" { + type = string + sensitive = true +} + +variable "pve_api_url" { type = string } +variable "pve_token" { + type = string + sensitive = true +} + +variable "node_name" { type = string } # e.g. "pve" +variable "datastore_id" { type = string } # e.g. "local-lvm" +variable "bridge" { type = string } # e.g. "vmbr0" + +variable "template_vmid" { type = number } # VMID of your template +variable "vm_id" { type = number } # VMID to assign +variable "name" { type = string } + +variable "ssh_pubkey_path" { type = string } # e.g. "~/.ssh/id_ed25519.pub" + +variable "admin_username" { + type = string + default = "azureuser" +} + +variable "azure_location" { + type = string + default = "westeurope" +} + +variable "azure_subscription_id" { + type = string +} + +variable "allowed_ssh_cidrs_ipv4" { + type = list(string) + description = "IPv4 CIDRs allowed to SSH (22/tcp). Empty list means allow from anywhere." + default = [] +}