migrate infra from single server to multi-server cluster

This commit is contained in:
Marcel Arndt
2025-12-12 11:41:32 +01:00
parent b1a70673a4
commit 156733f65a
63 changed files with 2219 additions and 3 deletions
@@ -0,0 +1,154 @@
package utils
import (
"bytes"
"fmt"
"regexp"
"strings"
"text/template"
"github.com/pulumi/pulumi-command/sdk/go/command/remote"
"github.com/pulumi/pulumi-hcloud/sdk/go/hcloud"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
type SwarmJoinTokens struct {
ManagerToken string
WorkerToken string
}
type ServerInfo struct {
Name pulumi.StringOutput
IP pulumi.StringOutput
}
func InstallAnsibleDependencies(ctx *pulumi.Context, connArgs remote.ConnectionArgs, uniqueness string) error {
_, err := remote.NewCommand(ctx, strings.Join([]string{uniqueness, "Install Ansible Dependencies"}, ": "),
&remote.CommandArgs{
Connection: connArgs,
Create: pulumi.String("apt-get update && apt-get install -y python3-pip python3-jsondiff"),
})
if err != nil {
return err
}
return nil
}
func InitDockerSwarm(ctx *pulumi.Context, connArgs remote.ConnectionArgs, advertiseAddr pulumi.StringOutput) (pulumi.Output, error) {
var tokens SwarmJoinTokens
fullCommand := advertiseAddr.ApplyT(func(addr string) *string {
initCommand := fmt.Sprintf("docker swarm init --advertise-addr %s", addr)
fullCommand := strings.Join([]string{initCommand, "echo \"Manager Token: $(docker swarm join-token -q manager)\"", "echo \"Worker Token: $(docker swarm join-token -q worker)\""}, " && ")
return &fullCommand
}).(pulumi.StringPtrOutput)
out, err := remote.NewCommand(ctx, "Init docker swarm",
&remote.CommandArgs{
Connection: connArgs,
Create: fullCommand,
})
if err != nil {
return pulumi.StringOutput{}, err
}
return out.Stdout.ApplyT(func(output string) SwarmJoinTokens {
searchWorker := "Worker Token: "
patternWorker := regexp.MustCompile(searchWorker + `(\S+)`)
searchManager := "Manager Token: "
patternManager := regexp.MustCompile(searchManager + `(\S+)`)
matches := patternWorker.FindStringSubmatch(output)
if len(matches) > 1 {
extracted := matches[1]
tokens.WorkerToken = extracted
}
matches = patternManager.FindStringSubmatch(output)
if len(matches) > 1 {
extracted := matches[1]
tokens.ManagerToken = extracted
}
return tokens
}), nil
}
func CreateAnsibleInventory(managerNodes, workerNodes []*hcloud.Server) (pulumi.Output, error) {
serverInfos := toServerInfo(managerNodes)
return pulumi.All(pulumi.ToOutput(serverInfos)).ApplyT(func(results []interface{}) (string, error) {
var serverInfos = results[0].([]ServerInfo)
// var workerSlice = results[1].([]*hcloud.Server)
serverData := make(map[string][]ServerInfo)
for _, s := range serverInfos {
serverData["Manager"] = append(serverData["Manager"], ServerInfo{
Name: s.Name,
IP: s.IP,
})
}
// for _, result := range workerSlice {
// server := result.(map[string]interface{})
// serverData["Worker"] = append(serverData["Worker"], ServerInfo{
// Name: server["name"].(string),
// IP: server["ipv4_address"].(string),
// })
// }
fmt.Println(serverData["Manager"])
fmt.Println(results[0])
return generateInventoryFile(serverData)
}).(pulumi.Output), nil
}
func toServerInfo(server []*hcloud.Server) pulumi.ArrayOutput {
serverInfo := []ServerInfo{}
for _, s := range server {
serverInfo = append(serverInfo, ServerInfo{
Name: s.Name,
IP: s.Ipv4Address,
})
}
return pulumi.All(serverInfo).ApplyT(func(args []interface{}) []interface{} {
var serverInfo []interface{}
for _, s := range args {
val := s.(map[string]interface{})
serverInfo = append(serverInfo, map[string]interface{}{
"Name": val["Name"].(string),
"IP": val["IP"].(string),
})
}
return serverInfo
}).(pulumi.ArrayOutput)
}
func generateInventoryFile(inventory map[string][]ServerInfo) (string, error) {
const inventoryTmpl = `
[all]
{{ range .Manager }}
{{ .Name }} ansible_host={{ .IP }} ansible_connection=ssh ansible_user=root ansible_ssh_private_key_file=../infra-base/private_key
{{ end }}
{{ range .Worker }}
{{ .Name }} ansible_host={{ .IP }} ansible_connection=ssh ansible_user=root ansible_ssh_private_key_file=../infra-base/private_key
{{ end }}
[manager]
{{ range .Manager }}
{{ .Name }} ansible_host={{ .IP }} ansible_connection=ssh ansible_user=root ansible_ssh_private_key_file=../infra-base/private_key
{{ end }}
[worker]
{{ range .Worker }}
{{ .Name }} ansible_host={{ .IP }} ansible_connection=ssh ansible_user=root ansible_ssh_private_key_file=../infra-base/private_key
{{ end }}
`
tmpl, err := template.New("inventory").Parse(inventoryTmpl)
if err != nil {
return "", err
}
var buf bytes.Buffer
err = tmpl.Execute(&buf, inventory)
if err != nil {
return "", err
}
return buf.String(), nil
}
@@ -0,0 +1,110 @@
package utils
import (
"avicenna-infra/config"
"fmt"
"net"
"github.com/pulumi/pulumi-hcloud/sdk/go/hcloud"
"github.com/pulumi/pulumi-tls/sdk/v5/go/tls"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func CreateSshKey(ctx *pulumi.Context) (*tls.PrivateKey, error) {
return tls.NewPrivateKey(ctx, "sshKey", &tls.PrivateKeyArgs{
Algorithm: pulumi.String("ED25519"),
}, pulumi.AdditionalSecretOutputs([]string{"privKey"}))
}
func CreatePlacementGroup(ctx *pulumi.Context, name string) (*hcloud.PlacementGroup, error) {
pg, err := hcloud.NewPlacementGroup(ctx, name, &hcloud.PlacementGroupArgs{
Name: pulumi.String(name),
Type: pulumi.String("spread"),
})
return pg, err
}
func CreateClusterNet(ctx *pulumi.Context, cfg config.InfraConfig) (*pulumi.IDOutput, error) {
var id pulumi.IDOutput
network, err := hcloud.NewNetwork(ctx, cfg.SwarmNetworkName, &hcloud.NetworkArgs{
Name: pulumi.String(cfg.SwarmNetworkName),
IpRange: pulumi.String(cfg.SwarmIpRange),
})
if err != nil {
return nil, err
}
id = network.ID()
_, err = hcloud.NewNetworkSubnet(ctx, "network-subnet", &hcloud.NetworkSubnetArgs{
Type: pulumi.String("cloud"),
NetworkId: IDtoIntOutput(id),
NetworkZone: pulumi.String("eu-central"),
IpRange: pulumi.String(cfg.SwarmSubnetIpRange),
})
if err != nil {
return nil, err
}
return &id, nil
}
type CreateServerArgs struct {
NetworkId pulumi.IDInput
PlacementGroupId pulumi.IDInput
// The first assignable IP of the network
NetworkFirstIP string
Basename string
Count int
SshKey *hcloud.SshKey
ServerType string
}
func CreateServer(ctx *pulumi.Context, cfg CreateServerArgs) ([]*hcloud.Server, error) {
var nodes []*hcloud.Server
nextIp := cfg.NetworkFirstIP
for i := range cfg.Count {
sn := fmt.Sprintf("%s-%d", cfg.Basename, i+1)
s, err := hcloud.NewServer(ctx, sn, &hcloud.ServerArgs{
Name: pulumi.String(sn),
Image: pulumi.String("docker-ce"),
ServerType: pulumi.String(cfg.ServerType),
Location: pulumi.StringPtr("fsn1"),
Networks: hcloud.ServerNetworkTypeArray{
&hcloud.ServerNetworkTypeArgs{
NetworkId: IDtoIntOutput(cfg.NetworkId),
Ip: pulumi.String(nextIp),
},
},
PlacementGroupId: IDtoIntPtrOutput(cfg.PlacementGroupId),
PublicNets: hcloud.ServerPublicNetArray{
&hcloud.ServerPublicNetArgs{
// Ipv4Enabled: pulumi.Bool(true),
Ipv6Enabled: pulumi.Bool(true),
},
},
SshKeys: pulumi.StringArray{cfg.SshKey.ID()},
})
if err != nil {
return nodes, err
}
cephVolume, err := hcloud.NewVolume(ctx, fmt.Sprintf("ceph-%s", sn), &hcloud.VolumeArgs{
Name: pulumi.Sprintf("%s-ceph-vol-0%d", s.Name, i+1),
Size: pulumi.Int(100),
Location: s.Location,
})
if err != nil {
return nodes, fmt.Errorf("couldn't create volume: %w", err)
}
_, err = hcloud.NewVolumeAttachment(ctx, fmt.Sprintf("ceph-vol-attach-%s", sn), &hcloud.VolumeAttachmentArgs{
VolumeId: IDtoIntOutput(cephVolume.ID()),
ServerId: IDtoIntOutput(s.ID()),
})
if err != nil {
return nodes, fmt.Errorf("couldn't attach volume to node %d", i)
}
nodes = append(nodes, s)
nextIp = IncrementIP(net.ParseIP(nextIp)).String()
}
return nodes, nil
}
@@ -0,0 +1,41 @@
package utils
import (
"net"
"strconv"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func IncrementIP(ip net.IP) net.IP {
if ip == nil {
return ip
}
ip = ip.To4()
ip[3]++
return ip
}
func IDtoInt(val string) (int, error) {
i, err := strconv.Atoi(val)
return i, err
}
func IDtoIntPtr(val string) (*int, error) {
i, err := strconv.Atoi(val)
return &i, err
}
// Convert pulumi.IDOutput to pulumi.InOutput
//
// Some hcloud ID's are int based, but pulumi.IDOutput doesn't have a ToIntOutput method
func IDtoIntOutput(val pulumi.IDInput) pulumi.IntOutput {
return val.ToIDOutput().ToStringOutput().ApplyT(IDtoInt).(pulumi.IntOutput)
}
// Convert pulumi.IDOutput to pulumi.InOutput
//
// Some hcloud ID's are int based, but pulumi.IDOutput doesn't have a ToIntOutput method
func IDtoIntPtrOutput(val pulumi.IDInput) pulumi.IntPtrOutput {
return val.ToIDOutput().ToStringOutput().ApplyT(IDtoIntPtr).(pulumi.IntPtrOutput)
}