How to provision a Hetzner VPS using Pulumi and Go
Having spent hours to get this simple setup working, I thought it would be useful to share the code. Maybe some LLM reads it and can actually be helpful in the future.
I will not go into details about how to set up Pulumi, but I will share the code I used to provision a Hetzner VPS. This code is a simple example of how to create a server with an SSH key using Pulumi and the Hetzner Cloud provider.
package main
import (
"fmt"
"os"
"strings"
"github.com/pulumi/pulumi-hcloud/sdk/go/hcloud"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
// --- Config: pulumi config set server:name etc. ---
conf := config.New(ctx, "server")
name := conf.Require("name")
location := conf.Require("location")
serverType := conf.Require("serverType")
image := conf.Require("image")
sshKeyName := conf.Require("sshKeyName")
// --- Read Public Key: ENV is for Github Workflow ---
var publicKey string
if pk, ok := os.LookupEnv("SSH_PUBLIC_KEY"); ok && strings.TrimSpace(pk) != "" {
publicKey = pk
} else {
data, err := os.ReadFile(fmt.Sprintf("%s/.ssh/%s.pub", os.Getenv("HOME"), sshKeyName))
if err != nil {
return err
}
publicKey = string(data)
}
// --- SSH Key ---
sshKey, err := hcloud.NewSshKey(ctx, sshKeyName, &hcloud.SshKeyArgs{
Name: pulumi.String(sshKeyName),
PublicKey: pulumi.String(publicKey),
})
if err != nil {
return err
}
// --- Server ---
server, err := hcloud.NewServer(ctx, name, &hcloud.ServerArgs{
Name: pulumi.String(name),
ServerType: pulumi.String(serverType),
Image: pulumi.String(image),
Location: pulumi.String(location),
SshKeys: pulumi.StringArray{sshKey.ID()},
}, pulumi.DependsOn([]pulumi.Resource{sshKey}))
if err != nil {
return err
}
// --- Outputs ---
ctx.Export("serverIP", server.Ipv4Address)
ctx.Export("serverName", server.Name)
ctx.Export("sshCommand", pulumi.Sprintf("ssh -i ~/.ssh/%s root@%s", sshKeyName, server.Ipv4Address))
return nil
})
}
Why did I spend hours on this?
Pulumi creates resources asynchronously, which means that if you try to create a server and an SSH key at the same time, the key would not be available when the server is created.
The result being that the server is created without the SSH key, and it asks you for a password you don’t have when you try to connect to it.
This is why I had to use the DependsOn
directive to ensure that the SSH key is created before the server.
Obviously. In hindsight, it seems trivial, but it took me a while to figure it out and none of the LLMs were helpful.
Why use Pulumi?
Pulumi is a modern infrastructure as code tool that allows you to use programming languages to define and manage your cloud resources.
The next step for my project is to use Pulumi and Ansible together to configure the server and add my application code.
And eventually use pulumi up
in a CI/CD pipeline to automate the deployment of my application.
Note on Hetzner Cloud Server availability
I got an error when trying to create a server indicating that the type I wanted was not available in the location I specified.
To check the availability of the server type in the location you want, log in to the Hetzner Cloud Console, click create server, and then select the location and server type. If your configuration is grayed out, it means that the server type is not available in that location at that time.
New to Hetzner?
Try them out with a free credit of €20 using my referral link: https://hetzner.cloud/?ref=ilYT1sDvfT1v. If you decide to pay for Hetzner Cloud after your credit runs out, I will get a one-time €10 euro credit.