defines network, subnets, cloudflare dns & floatingip

This commit is contained in:
2025-12-31 14:46:53 +01:00
parent 2bb876904f
commit e65aead5f0
8 changed files with 312 additions and 84 deletions

View File

@@ -2,9 +2,19 @@ import {
subNetwork, subNetwork,
regionalNetwork, regionalNetwork,
allowHttp, allowHttp,
allowSSH, allowSSHToCurrentIP,
floatingIP,
attach,
} from "./resources/network"; } from "./resources/network";
import { server } from "./resources/compute"; import { server } from "./resources/compute";
import { dns } from "./resources/cloudflare";
import {
summarizeServer,
summarizeNetwork,
summarizeSubNetwork,
summarizeFloatingIp,
summarizeFirewall,
} from "./resources/utils";
import { import {
VmSize, VmSize,
@@ -15,15 +25,19 @@ import {
} from "./resources/types"; } from "./resources/types";
// regional vnet // regional vnet
const eu = regionalNetwork("ha", "10.24.0.0/18", NetworkRegion.eu); const eu = regionalNetwork("ha-net-eu", "10.24.0.0/18", NetworkRegion.eu);
const usEast = regionalNetwork("ha", "10.25.0.0/18", NetworkRegion.usEast); const usEast = regionalNetwork(
"ha-net-us",
"10.25.0.0/18",
NetworkRegion.usEast,
);
// subnets for reginal vnets // subnets for reginal vnets
const network = { const network = {
eu: { eu: {
lb: subNetwork(eu, NetworkRole.lb, NetworkRegion.eu, "10.24.1.0/24"), lb: subNetwork(eu, NetworkRole.lb, NetworkRegion.eu, "10.24.1.0/26"),
cache: subNetwork(eu, NetworkRole.cache, NetworkRegion.eu, "10.24.2.0/24"), cache: subNetwork(eu, NetworkRole.cache, NetworkRegion.eu, "10.24.2.0/26"),
web: subNetwork(eu, NetworkRole.web, NetworkRegion.eu, "10.24.3.0/24"), web: subNetwork(eu, NetworkRole.web, NetworkRegion.eu, "10.24.3.0/26"),
// db: subNetwork(eu, NetworkRole.db, "10.24.4.0/24") // db: subNetwork(eu, NetworkRole.db, "10.24.4.0/24")
}, },
usEast: { usEast: {
@@ -31,26 +45,26 @@ const network = {
usEast, usEast,
NetworkRole.lb, NetworkRole.lb,
NetworkRegion.usEast, NetworkRegion.usEast,
"10.25.1.0/24", "10.25.1.0/26",
), ),
cache: subNetwork( cache: subNetwork(
usEast, usEast,
NetworkRole.cache, NetworkRole.cache,
NetworkRegion.usEast, NetworkRegion.usEast,
"10.25.2.0/24", "10.25.2.0/26",
), ),
web: subNetwork( web: subNetwork(
usEast, usEast,
NetworkRole.web, NetworkRole.web,
NetworkRegion.usEast, NetworkRegion.usEast,
"10.25.3.0/24", "10.25.3.0/26",
), ),
}, },
}; };
// variable un-maps // variable un-maps
const hel1 = ServerLocations.helsinki; const nbg = ServerLocations.nuremberg;
const hil = ServerLocations.hillsboro; const ash = ServerLocations.ashburn;
const [EU_LB, US_LB, EU_CACHE, US_CACHE, EU_WEB, US_WEB] = [ const [EU_LB, US_LB, EU_CACHE, US_CACHE, EU_WEB, US_WEB] = [
network.eu.lb, network.eu.lb,
network.usEast.lb, network.usEast.lb,
@@ -61,32 +75,67 @@ const [EU_LB, US_LB, EU_CACHE, US_CACHE, EU_WEB, US_WEB] = [
]; ];
// compute - server resources // compute - server resources
const haEU1 = server("haproxy-1", VmSize.small, OS.debian, hel1, EU_LB); const haEU1 = server("haproxy-1", VmSize.cx23, OS.debian, nbg, EU_LB, true);
const haEU2 = server("haproxy-2", VmSize.small, OS.debian, hel1, EU_LB); const haEU2 = server("haproxy-2", VmSize.cx23, OS.debian, nbg, EU_LB, true);
const haUS1 = server("haproxy-1", VmSize.small, OS.debian, hil, US_LB); const haUS1 = server("haproxy-1", VmSize.cpx11, OS.debian, ash, US_LB, true);
// const haUS2 = server("haproxy-2", VmSize.small, OS.debian, hil, US_LB); const haUS2 = server("haproxy-2", VmSize.cpx11, OS.debian, ash, US_LB, true);
const cacheEU1 = server("varnish-1", VmSize.small, OS.debian, hel1, EU_CACHE); const cacheEU1 = server("varnish-1", VmSize.cx23, OS.debian, nbg, EU_CACHE);
const cacheEU2 = server("varnish-2", VmSize.small, OS.debian, hil, EU_CACHE); const cacheEU2 = server("varnish-2", VmSize.cx23, OS.debian, nbg, EU_CACHE);
// const cacheUS1 = server("varnish-1", VmSize.small, OS.debian, hil, US_CACHE); const cacheUS1 = server("varnish-1", VmSize.cpx11, OS.debian, ash, US_CACHE);
// const cacheUS2 = server("varnish-2", VmSize.small, OS.debian, hil, US_CACHE); const cacheUS2 = server("varnish-2", VmSize.cpx11, OS.debian, ash, US_CACHE);
const webEU1 = server("web-1", VmSize.small, OS.debian, hel1, EU_WEB); const webEU1 = server("web-1", VmSize.cx23, OS.debian, nbg, EU_WEB);
// const webEU2 = server("web-2", VmSize.small, OS.debian, hel1, EU_WEB); const webEU2 = server("web-2", VmSize.cx23, OS.debian, nbg, EU_WEB);
// const webUS1 = server("web-1", VmSize.small, OS.debian, hil, US_WEB); const webUS1 = server("web-1", VmSize.cpx11, OS.debian, ash, US_WEB);
// firewall & exports // floating IPs
export const firewalls = [allowHttp, allowSSH]; const euFloatingIP = floatingIP("schleppe-ha-nbg", haEU1);
const usFloatingIP = floatingIP("schleppe-ha-va", haUS1);
const floatingIPs = [euFloatingIP, usFloatingIP];
const domains = ["k9e.no", "planetposen.no", "whoami.schleppe.cloud"];
// exports contd. // Update Cloudflare DNS
export const servers = [haEU1, haEU2, haUS1, cacheEU1, cacheEU2, webEU1]; domains.forEach((domain) => {
dns(domain, euFloatingIP, "eu-fip");
dns(domain, usFloatingIP, "us-fip");
});
export const networks = [ // firewall
eu, const allowSSH = allowSSHToCurrentIP();
usEast, const firewalls = [allowHttp, allowSSH];
// DISABLED
attach("ssh-fa", allowSSH, [haEU1, haEU2, haUS1, haUS2]);
// exports
const servers = [
haEU1,
haEU2,
haUS1,
haUS2,
cacheEU1,
cacheEU2,
cacheUS1,
cacheUS2,
webEU1,
webEU2,
webUS1,
];
const networks = [eu, usEast];
const subNetworks = [
network.eu.lb, network.eu.lb,
network.eu.cache, network.eu.cache,
network.eu.web, network.eu.web,
network.usEast.lb, network.usEast.lb,
network.usEast.web, network.usEast.web,
]; ];
export const inventory = {
vms: servers.map(summarizeServer),
networks: networks.map(summarizeNetwork),
subnetworks: subNetworks.map(summarizeSubNetwork),
firewalls: firewalls.map(summarizeFirewall),
floatingIps: floatingIPs.map(summarizeFloatingIp),
domains,
};

View File

@@ -6,6 +6,7 @@
"typescript": "^5.0.0" "typescript": "^5.0.0"
}, },
"dependencies": { "dependencies": {
"@pulumi/cloudflare": "^6.12.0",
"@pulumi/hcloud": "^1.29.0", "@pulumi/hcloud": "^1.29.0",
"@pulumi/pulumi": "^3.213.0", "@pulumi/pulumi": "^3.213.0",
"@pulumi/random": "^4.18.4", "@pulumi/random": "^4.18.4",

View File

@@ -0,0 +1,44 @@
import * as hcloud from "@pulumi/hcloud";
import * as cloudflare from "@pulumi/cloudflare";
async function getZone(domain: string): Promise<cloudflare.Zone | null> {
let match;
const zones = await cloudflare.getZones();
zones.results.forEach((zone) => {
if (domain.includes(zone.name)) match = zone;
});
if (match) return match;
return null;
}
export async function dns(
domain: string,
ipAddress: hcloud.FloatingIp,
suffix: string,
) {
const ip = ipAddress.ipAddress.apply((ip) => ip);
const name = `${domain}-${suffix}_dns_record`;
const comment = "managed by pulumi - schleppe-ha-project";
const zone = await getZone(domain);
if (!zone)
throw new Error(
"no matching zone found! check cloudflare token scopes & registration",
);
return new cloudflare.DnsRecord(
name,
{
zoneId: zone.id,
name: domain,
ttl: 1,
type: "A",
content: ip,
proxied: false,
comment,
},
{ dependsOn: [ipAddress] },
);
}

View File

@@ -1,51 +1,86 @@
import * as pulumi from "@pulumi/pulumi"; import * as pulumi from "@pulumi/pulumi";
import * as hcloud from "@pulumi/hcloud"; import * as hcloud from "@pulumi/hcloud";
import * as random from "@pulumi/random"; import * as random from "@pulumi/random";
import { config } from './config'; import { config } from "./config";
import { getCheapestServerType } from './utils'; import { getCheapestServerType, topicedLabel } from "./utils";
import { VmSize, OS, ServerLocations } from "./types"; import { VmSize, OS, ServerLocations } from "./types";
// “Tag” servers using labels. Hetzner firewalls can target servers by label selectors. :contentReference[oaicite:2]{index=2} // “Tag” servers using labels. Hetzner firewalls can target servers by label selectors. :contentReference[oaicite:2]{index=2}
const serverLabels = { const serverLabels = {
app: "demo",
role: "web",
env: pulumi.getStack(), env: pulumi.getStack(),
managed: "pulumi",
}; };
const sshPublicKey = config.require("sshPublicKey"); const sshPublicKey = config.require("sshPublicKey");
const sshKey = new hcloud.SshKey("ssh-key", { const sshKey = new hcloud.SshKey("ssh-key", {
name: `pulumi-${pulumi.getStack()}-ssh`, name: `pulumi-${pulumi.getStack()}-ssh`,
publicKey: sshPublicKey, publicKey: sshPublicKey,
}); });
const serverName = (name: string, location: string) => {
if (name.includes("-")) {
const [n, id] = name.split("-");
return `${n}-${location}-${id}`;
}
return `${name}-${location}`;
};
export function server( export function server(
name: string, name: string,
size: VmSize, size: VmSize,
os: OS = OS.debian, os: OS = OS.debian,
location: ServerLocations, location: ServerLocations,
network: hcloud.NetworkSubnet network: hcloud.NetworkSubnet,
ipv4: boolean = false,
): hcloud.Server { ): hcloud.Server {
const ceap = getCheapestServerType('eu'); const extraLabel = topicedLabel(name)
name = serverName(name, location);
const networkId = network.networkId.apply((id) => String(id).split("-")[0]);
const hexId = new random.RandomId(`${name}-${location}`, { const server = new hcloud.Server(
byteLength: 2, // 2 bytes = 4 hex characters name,
}); {
name = `${name}-${location}`
return new hcloud.Server(name, {
name, name,
image: os, image: os,
serverType: ceap, serverType: size,
location, location,
backups: false, backups: false,
publicNets: [{ publicNets: [
ipv4Enabled: false, {
ipv4Enabled: ipv4,
ipv6Enabled: true, ipv6Enabled: true,
}], },
networks: [network], ],
networks: [
{
networkId: networkId.apply((nid) => Number(nid)),
},
],
sshKeys: [sshKey.name], sshKeys: [sshKey.name],
labels: serverLabels labels: {
}) ...serverLabels,
...extraLabel
},
},
{ dependsOn: [network] },
);
const serverNet = new hcloud.ServerNetwork(
`${name}-servernet-${location}`,
{
serverId: server.id.apply((id) => Number(id)),
subnetId: network.id,
},
{
dependsOn: [network, server],
parent: server,
deleteBeforeReplace: true,
ignoreChanges: [ 'serverId', 'ip', 'aliasIps', 'networkId', 'subnetId' ]
},
);
return server;
} }

View File

@@ -2,24 +2,22 @@ import * as pulumi from "@pulumi/pulumi";
import * as hcloud from "@pulumi/hcloud"; import * as hcloud from "@pulumi/hcloud";
import type { NetworkRegion } from "./types"; import type { NetworkRegion } from "./types";
import { currentIPAddress } from "./utils";
// Required // NETWORKS
// make sure to have regional parent networks
const networkName = (name: string, region: NetworkRegion) => const networkName = (name: string, region: NetworkRegion) =>
`${name}-net-${region}`; `${name}-net-${region}`;
export function regionalNetwork( export function regionalNetwork(
prefix: string, name: string,
cidr: string, cidr: string,
region: NetworkRegion, region: NetworkRegion,
) { ) {
const name = networkName(prefix, region); const parentNetworkRange = 22;
const parentNetworkRange = 8;
const [ip, _] = cidr.split("/"); const [ip, _] = cidr.split("/");
const net = new hcloud.Network(name, { const net = new hcloud.Network(name, {
name,
ipRange: `${ip}/${parentNetworkRange}`, ipRange: `${ip}/${parentNetworkRange}`,
labels: { labels: {
region, region,
@@ -38,16 +36,33 @@ export function subNetwork(
): hcloud.NetworkSubnet { ): hcloud.NetworkSubnet {
const name = `${prefix}-subnet-${region}`; const name = `${prefix}-subnet-${region}`;
const net = new hcloud.NetworkSubnet(name, { const net = new hcloud.NetworkSubnet(
networkId: parentNetwork.id.apply(id => Number(id)), name,
{
networkId: parentNetwork.id.apply((id) => Number(id)),
type: "cloud", type: "cloud",
networkZone: "eu-central", networkZone: region,
ipRange: cidr, ipRange: cidr,
}); },
{ parent: parentNetwork, dependsOn: [parentNetwork] },
);
return net; return net;
} }
// FLOATING IPs
export function floatingIP(name: string, server: hcloud.Server) {
return new hcloud.FloatingIp(
name,
{
type: "ipv4",
serverId: server.id.apply((i) => Number(i)),
},
{ dependsOn: [server] },
);
}
// FIREWALL RULES
export const allowHttp = new hcloud.Firewall("allow-http", { export const allowHttp = new hcloud.Firewall("allow-http", {
name: "allow-http", name: "allow-http",
applyTos: [ applyTos: [
@@ -80,16 +95,30 @@ export const allowHttp = new hcloud.Firewall("allow-http", {
], ],
}); });
export const allowSSH = new hcloud.Firewall("allow-ssh", { export function allowSSHToCurrentIP() {
const ip = currentIPAddress()
return new hcloud.Firewall("allow-ssh", {
name: "allow-ssh", name: "allow-ssh",
rules: [ rules: [
{ {
direction: "in", direction: "in",
protocol: "tcp", protocol: "tcp",
port: "22", port: "22",
sourceIps: ["127.0.0.0/24"], sourceIps: [ip],
description: "Allow SSH from approved CIDRs only", description: "Allow SSH from approved CIDRs only",
}, },
], ],
}); });
}
export function attach(
name: string,
firewall: hcloud.Firewall,
servers: hcloud.Server[],
) {
return new hcloud.FirewallAttachment(name, {
firewallId: firewall.id.apply((id) => Number(id)),
serverIds: servers.map((server) => server.id.apply((id) => Number(id))),
});
}

View File

@@ -2,10 +2,13 @@ export enum VmSize {
small = "small", small = "small",
medium = "medium", medium = "medium",
large = "large", large = "large",
cx23 = "cx23",
cax11 = "cax11",
cpx11 = "cpx11"
} }
export enum OS { export enum OS {
debian = "debian", debian = "debian-13",
ubuntu = "ubuntu", ubuntu = "ubuntu",
} }

View File

@@ -1,4 +1,5 @@
import * as pulumi from "@pulumi/pulumi"; import * as pulumi from "@pulumi/pulumi";
import * as hcloud from "@pulumi/hcloud";
import { z } from "zod"; import { z } from "zod";
import * as crypto from "node:crypto"; import * as crypto from "node:crypto";
@@ -103,3 +104,62 @@ export function getCheapestServerType(
}); });
} }
interface Label {
role?: string
}
export function topicedLabel(name: string) {
let labels: Label = {};
if (name.includes("haproxy")) {
labels.role = 'load-balancer';
} else if (name.includes("web")) {
labels.role = 'web'
}
return labels
}
export const summarizeServer = (s: hcloud.Server) => ({
name: s.name,
publicIpv4: s.ipv4Address,
publicIpv6: s.ipv6Address,
privateIp: s.networks.apply(nets => nets?.[0]?.ip ?? 'null'),
});
export const summarizeNetwork = (n: hcloud.Network) => ({
name: n.name,
cidr: n.ipRange
});
export const summarizeSubNetwork = (n: hcloud.NetworkSubnet) => ({
gateway: n.gateway,
cidr: n.ipRange,
zone: n.networkZone,
type: n.type
});
export const summarizeFloatingIp = (floatingIp: hcloud.FloatingIp) => ({
name: floatingIp.name,
address: floatingIp.ipAddress,
attachedTo: floatingIp.serverId,
location: floatingIp.homeLocation,
labels: floatingIp.labels
})
export const summarizeFirewall = (firewall: hcloud.Firewall) => ({
name: firewall.name,
rules: firewall.rules,
labels: firewall.labels
})
export const summarizeDns = (firewall: hcloud.Firewall) => ({
name: firewall.name,
rules: firewall.rules,
labels: firewall.labels
})
export async function currentIPAddress(): Promise<string> {
return fetch('https://ifconfig.me/ip')
.then(resp => resp.text())
}

View File

@@ -369,6 +369,13 @@
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
"@pulumi/cloudflare@^6.12.0":
version "6.12.0"
resolved "https://registry.yarnpkg.com/@pulumi/cloudflare/-/cloudflare-6.12.0.tgz#edb2c59d1b594a0c64f12ba8cf63b84ed3937fcc"
integrity sha512-Evhj9u+ZVCO6Cu9+xon4F3dY4RA7U4ZTWc19MJAIa5GgLeG8X0C84iyo5aruZf2nK7nfJ2d47lyRSaXPBHOzjw==
dependencies:
"@pulumi/pulumi" "^3.142.0"
"@pulumi/hcloud@^1.29.0": "@pulumi/hcloud@^1.29.0":
version "1.29.0" version "1.29.0"
resolved "https://registry.yarnpkg.com/@pulumi/hcloud/-/hcloud-1.29.0.tgz#4c622aa3ae9c4590af783eefbeee1e62e711c7f3" resolved "https://registry.yarnpkg.com/@pulumi/hcloud/-/hcloud-1.29.0.tgz#4c622aa3ae9c4590af783eefbeee1e62e711c7f3"