Having a small project to deploy at Hetzner, I decided to try using CDK for the first time outside of AWS. Hashicorp’s CDKTF rolls AWS CDK with Terraform for doing IAC with the many TF providers available.

Getting started

I followed the install instructions for cdktf itself with no issues. I used asdf to install nodejs 20.11.0. cdktf init with typescript as the language.

Authentication

A bit of clicking around in Hetzner’s screens leads to the API key tool where a bearer token string can be generated. For direct API use, which I tried to make sure it was right it is a standard authorization: bearer <token> http header. I wrote the plain token to a file, which is useful later.

The Stack

Install the hcloud/provider and add the provider to your main.ts:

const provider = new HcloudProvider(this, "hetzner", {
})

Don’t put your token into the provider, it will end up in the cdk.out files and depending on how you store them may pose a security issue.

Instead, when you run cdktf, do something like:

HCLOUD_TOKEN=$(<token ) cdktf diff

Hetzner is organized by datacenters

SSH Keys

Injecting ssh keys into servers requires creating SshKey resources:

const sshPublicKeys = [
      [
	    "danj",
        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKw9W05dLNGIH1jM9ceezvbLx/I2nTAeP3ZIU4Hsm+o9 danj"
	  ]
    ];

    sshPublicKeys.map(([name,key],idx) => new SshKey(this, `ssh-${idx}`, {
      name,
      publicKey: key
    }));

Firewall

You probably want one of them

    new Firewall(this, "firewall", {
      name: "stack-fw",
      rule: [
	{
	  description: "ssh from office",
	  direction: "in",
	  protocol: "tcp",
	  port: "22",
	  sourceIps
	},
	{
	  description: "icmp from office",
	  direction: "in",
	  protocol: "icmp",
	  sourceIps
	}
      ],
      applyTo: [
        { labelSelector: "office-only" },
      ]
    });

The applyTo->labelSelector creates a forward association with servers that you want to tie to this firewall, as servers come up if the labelSelector matches it will adopt this firewall.

userData

Update and install some software while the server comes up with cloud-init. The first line has to be a #! or the data will not be given the right mime-type and won’t run.

    const userData = [
      "#!/bin/bash",
      "export NEEDRESTART_MODE=a",
      "apt -y update",
      "apt -y upgrade",
      "apt -y install docker.io docker-compose docker-buildx",
    ].join("\n")

Server

The labels need to match the firewall selector, but the docs are not clear on if the match is by key or value. sshKeys are by name of the resource. I tried to do only IPv6, but my network isn’t properly configured for it, so that’s a battle for later.

    const server = new Server(this, "server", {
      provider,
      labels: { "office-only": "office-only" },
      backups: false,
      datacenter,
      name: "office-services",
      image: "ubuntu-22.04",
      serverType: "cpx11",
      userData,
      sshKeys: ["danj"],
      publicNet: [
	    {
	      ipv4Enabled: true,
	      ipv6Enabled: true
  	    }
      ]
    });

Volume

For a bit of storage, a direct attach block store volume. The size is in GiB, deletionProtection set to true will prevent cdktf destroy from completing, it does work.

    new Volume(this, "office-data", {
      name: "office-data",
      size: 10,
      automount: true,
      format: "xfs",
      deleteProtection: true,
      serverId: Fn.tonumber(server.id),
    });

The important takeaway is Fn.tonumber. It took quite a while to find any hint about how to use/refer to a materialized id that also is not the right type. Using TS to convert the value doesn’t work because it is too soon. Fn.tonumber is getting passed as a function into materialization.

I’d not seen this in AWS since ids are all strings. But for Hetzner the serverId parameter for Volume is a number but the type of server.id is a string.

The Volume, with automount: true, is mounted on start up, although the exact moment (before or after cloud-init runs user-data) is unknown to me. The default mount is like /mnt/HC_Volume_100314108

Resources

https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs

https://developer.hashicorp.com/terraform/language/functions

https://developer.hashicorp.com/terraform/cdktf