393 lines
13 KiB
TypeScript
393 lines
13 KiB
TypeScript
|
|
// 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<string>;
|
||
|
|
vswitchId?: number;
|
||
|
|
sshKeyPath: string;
|
||
|
|
datacenter: string;
|
||
|
|
servers: {
|
||
|
|
name: string;
|
||
|
|
hostname: string;
|
||
|
|
image?: string;
|
||
|
|
type: string;
|
||
|
|
labels?: Record<string, string>;
|
||
|
|
cloudInitConfig?: GetCloudInitConfigArgs;
|
||
|
|
}[];
|
||
|
|
loadbalancers?: {
|
||
|
|
name: string;
|
||
|
|
services: {
|
||
|
|
name: string;
|
||
|
|
port: number;
|
||
|
|
targetPort: number;
|
||
|
|
proxyprotocol?: boolean;
|
||
|
|
healthCheck?: LoadBalancerServiceHealthCheck;
|
||
|
|
}[];
|
||
|
|
targets: Partial<hcloud.LoadBalancerTargetArgs>[];
|
||
|
|
}[];
|
||
|
|
}
|
||
|
|
|
||
|
|
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<string>;
|
||
|
|
publicIp: pulumi.Output<string>;
|
||
|
|
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<GetDatacentersResult>
|
||
|
|
function getDatacentersOutput(args: GetDatacentersOutputArgs, opts?: InvokeOptions): Output<GetDatacentersResult>
|
||
|
|
|
||
|
|
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 };
|