// hetzner/index.ts import * as pulumi from "@pulumi/pulumi"; import * as hcloud from "@pulumi/hcloud"; import * as fs from "fs"; import { getCloudInitConfig, type GetCloudInitConfigArgs, } from "./cloudinit-config"; import { LoadBalancerServiceHealthCheck } from "@pulumi/hcloud/types/input"; interface HetznerDeploymentConfig { platformName: string; datacenter: string; } interface HetznerDeploymentArgs { platformName: string; hcloudToken: pulumi.Input; vswitchId?: number; sshKeyPath: string; datacenter: string; servers: { name: string; hostname: string; image?: string; type: string; labels?: Record; cloudInitConfig?: GetCloudInitConfigArgs; }[]; loadbalancers?: { name: string; services: { name: string; port: number; targetPort: number; proxyprotocol?: boolean; healthCheck?: LoadBalancerServiceHealthCheck; }[]; targets: Partial[]; }[]; } class HetznerDeployment extends pulumi.ComponentResource { public readonly deploymentConfig: HetznerDeploymentConfig; public readonly servers: hcloud.Server[]; public readonly platformNetwork: hcloud.Network; public readonly platformNetworkSubnetCloud: hcloud.NetworkSubnet; public readonly platformNetworkSubnetDedicated?: | hcloud.NetworkSubnet | undefined; public readonly platformNetworkSubnetLoadBalancers: hcloud.NetworkSubnet; public readonly publicIps: hcloud.PrimaryIp[]; public readonly placementGroup: hcloud.PlacementGroup; public readonly networkAttachments: hcloud.ServerNetwork[]; // public readonly loadBalancers: hcloud.LoadBalancer[]; public readonly loadBalancerInstances: { loadBalancer: hcloud.LoadBalancer; networkAttachment: hcloud.LoadBalancerNetwork; services: hcloud.LoadBalancerService[]; targets: hcloud.LoadBalancerTarget[]; }[]; public readonly serverInfo: { name: pulumi.Output; publicIp: pulumi.Output; privateIp: string; }[]; constructor( name: string, args: HetznerDeploymentArgs, opts?: pulumi.ComponentResourceOptions, ) { super("custom:resource:HetznerDeployment", name, {}, opts); this.deploymentConfig = args; // Hetzner Provider const hcloudProvider = new hcloud.Provider( `${this.deploymentConfig.platformName}-hcloud-provider`, { token: args.hcloudToken, }, { parent: this }, ); /* Hetzner API has 2 functions to get datacenters: function getDatacenters(args: GetDatacentersArgs, opts?: InvokeOptions): Promise function getDatacentersOutput(args: GetDatacentersOutputArgs, opts?: InvokeOptions): Output We need to check wether this.deploymentConfig.datacenter is a valid datacenter. */ hcloud .getDatacentersOutput({ provider: hcloudProvider, parent: this }) .apply((datacenters) => { const isValid = datacenters.datacenters.some( (dc) => dc.name === this.deploymentConfig.datacenter, ); if (!isValid) { throw new Error( `Invalid datacenter: ${this.deploymentConfig.datacenter}, valid datacenters are: ${datacenters.datacenters .map((dc) => dc.name) .join(", ")}`, ); } }); const sshPublicKey = fs.readFileSync(args.sshKeyPath, "utf-8"); const sshKey = new hcloud.SshKey( `${this.deploymentConfig.platformName}-ssh-key`, { name: `${this.deploymentConfig.platformName}-ssh-key`, publicKey: sshPublicKey, }, { parent: this, provider: hcloudProvider, dependsOn: [hcloudProvider], deleteBeforeReplace: true, }, ); this.placementGroup = new hcloud.PlacementGroup( `${this.deploymentConfig.platformName}-hcloud-placement-group`, { name: `${this.deploymentConfig.platformName}-hcloud-placement-group`, type: "spread", labels: { platform: this.deploymentConfig.platformName, }, }, { parent: this, provider: hcloudProvider, dependsOn: [hcloudProvider] }, ); // Allocate public IPs this.publicIps = args.servers.map((server, serverIndex) => { return new hcloud.PrimaryIp( `${this.deploymentConfig.platformName}-hcloud-primary-ip-${(serverIndex + 1).toString().padStart(2, "0")}`, { type: "ipv4", assigneeType: "server", autoDelete: false, datacenter: this.deploymentConfig.datacenter, labels: { platform: this.deploymentConfig.platformName, role: "controlplane", node: server.hostname, }, }, { parent: this, provider: hcloudProvider, dependsOn: [hcloudProvider], }, ); }); // Create a network this.platformNetwork = new hcloud.Network( `${this.deploymentConfig.platformName}-hcloud-network`, { name: `${this.deploymentConfig.platformName}-hcloud-network`, ipRange: "10.254.0.0/16", labels: { platform: this.deploymentConfig.platformName, }, deleteProtection: false, }, { parent: this, provider: hcloudProvider, dependsOn: [hcloudProvider] }, ); // Create a network subnet for cloud vms this.platformNetworkSubnetCloud = new hcloud.NetworkSubnet( `${this.deploymentConfig.platformName}-hcloud-network-subnet-cloud`, { type: "cloud", networkId: this.platformNetwork.id.apply((id) => Number(id)), networkZone: "eu-central", ipRange: "10.254.1.0/24", }, { parent: this, provider: hcloudProvider, dependsOn: [hcloudProvider, this.platformNetwork], }, ); if (args.vswitchId) { // Create a network subnet for dedicated servers this.platformNetworkSubnetDedicated = new hcloud.NetworkSubnet( `${this.deploymentConfig.platformName}-hcloud-network-subnet-dedicated`, { type: "vswitch", networkId: this.platformNetwork.id.apply((id) => Number(id)), networkZone: "eu-central", ipRange: "10.254.2.0/24", vswitchId: args.vswitchId, }, { parent: this, provider: hcloudProvider, dependsOn: [hcloudProvider, this.platformNetwork], }, ); } this.servers = args.servers.map((server, serverIndex) => { return new hcloud.Server( `${this.deploymentConfig.platformName}-hcloud-server-${(serverIndex + 1).toString().padStart(2, "0")}-${server.name}`, { name: server.name, serverType: server.type, deleteProtection: false, image: server.image || "debian-12", datacenter: this.deploymentConfig.datacenter, placementGroupId: this.placementGroup.id.apply((id) => Number(id)), sshKeys: [sshKey.id], publicNets: [ { ipv4Enabled: true, ipv6Enabled: false, ipv4: this.publicIps[serverIndex].id.apply((id) => Number(id)), }, ], labels: { platform: this.deploymentConfig.platformName, ...server.labels, }, userData: getCloudInitConfig({ ...(server.cloudInitConfig || {}), sshAuthorizedKeys: [sshPublicKey], }), }, { parent: this, provider: hcloudProvider, ignoreChanges: ["userData"], dependsOn: [ this.placementGroup, this.platformNetwork, this.platformNetworkSubnetCloud, this.publicIps[serverIndex], sshKey, ], }, ); }); this.networkAttachments = this.servers.map((server, serverIndex) => { return new hcloud.ServerNetwork( `${this.deploymentConfig.platformName}-hcloud-server-network-${(serverIndex + 1).toString().padStart(2, "0")}-${args.servers[serverIndex].name}`, { serverId: server.id.apply((id) => Number(id)), networkId: this.platformNetworkSubnetCloud.networkId.apply((id) => Number(id), ), ip: `10.254.1.${serverIndex + 1 + 10}`, }, { parent: this, provider: hcloudProvider, dependsOn: [ this.platformNetwork, this.publicIps[serverIndex], this.servers[serverIndex], ], deletedWith: this.servers[serverIndex], }, ); }); // Create a network subnet for dedicated servers this.platformNetworkSubnetLoadBalancers = new hcloud.NetworkSubnet( `${this.deploymentConfig.platformName}-hcloud-network-subnet-loadbalancers`, { type: "cloud", networkId: this.platformNetwork.id.apply((id) => Number(id)), networkZone: "eu-central", ipRange: "10.254.3.0/24", }, { parent: this, provider: hcloudProvider, dependsOn: [hcloudProvider, this.platformNetwork], }, ); this.loadBalancerInstances = args.loadbalancers?.map((loadBalancerArg, loadBalancerIndex) => { const loadBalancer = new hcloud.LoadBalancer( `${this.deploymentConfig.platformName}-hcloud-loadbalancer-${loadBalancerIndex}-${loadBalancerArg.name}`, { name: loadBalancerArg.name, loadBalancerType: "lb11", networkZone: "eu-central", }, { parent: this, provider: hcloudProvider, dependsOn: [ this.platformNetwork, this.platformNetworkSubnetLoadBalancers, ], }, ); const networkAttachment = new hcloud.LoadBalancerNetwork( `${this.deploymentConfig.platformName}-hcloud-loadbalancer-network-${loadBalancerIndex}-${loadBalancerArg.name}`, { subnetId: this.platformNetworkSubnetLoadBalancers.id.apply( (id) => id, ), // networkId: this.platformNetwork.id.apply((id) => Number(id)), loadBalancerId: loadBalancer.id.apply((id) => Number(id)), }, { parent: this, provider: hcloudProvider, dependsOn: [ loadBalancer, this.platformNetwork, this.platformNetworkSubnetLoadBalancers, ], }, ); const loadBalancerTargets = loadBalancerArg.targets.map( (target, targetIndex) => { return new hcloud.LoadBalancerTarget( `${this.deploymentConfig.platformName}-hcloud-loadbalancer-target-${loadBalancerIndex}-${loadBalancerArg.name}-${targetIndex}`, { type: target.type || "ip", loadBalancerId: loadBalancer.id.apply((id) => Number(id)), ...target, }, { parent: this, provider: hcloudProvider, dependsOn: [loadBalancer], }, ); }, ); const loadBalancerServices = loadBalancerArg.services.map( (service, serviceIndex) => { return new hcloud.LoadBalancerService( `${this.deploymentConfig.platformName}-hcloud-loadbalancer-service-${loadBalancerIndex}-${loadBalancerArg.name}-${serviceIndex}`, { loadBalancerId: loadBalancer.id.apply((id) => id), protocol: "tcp", listenPort: service.port, destinationPort: service.targetPort, proxyprotocol: service.proxyprotocol || false, healthCheck: service.healthCheck || { port: service.targetPort, protocol: "tcp", interval: 15, timeout: 10, retries: 3, }, }, { parent: this, provider: hcloudProvider, dependsOn: [loadBalancer], deleteBeforeReplace: true, }, ); }, ); return { loadBalancer: loadBalancer, networkAttachment: networkAttachment, targets: loadBalancerTargets, services: loadBalancerServices, }; }) || []; this.serverInfo = this.servers.map((server, serverIndex) => { return { name: server.name, publicIp: this.publicIps[serverIndex].ipAddress, privateIp: `10.254.1.${serverIndex + 1 + 10}`, }; }); this.registerOutputs({ servers: this.servers, platformNetwork: this.platformNetwork, publicIps: this.publicIps, serverInfo: this.serverInfo, networkAttachments: this.networkAttachments, }); } } export { HetznerDeployment };