Deploying the blog

Alright, now that we have a simple web server built in go, we’ll have to make this public! I’ve been using Hetzner Cloud for personal projects. So we’ll use this again. But I don’t want to have to manually configure a VM host. Instead, we’ll use Terraform.

Terraform setup

Let’s create a directory called infra and a main.tf file within. The first step is to setup the hcloud provider:

terraform {
  required_providers {
    hcloud = {
      source = "hetznercloud/hcloud"
      version = "~> 1.45"
    }
  }
}

variable "hcloud_token" {
  sensitive = true
}

provider "hcloud" {
  token = var.hcloud_token
}

And then we can go to the hcloud console, create a project, and create an API token. We’ll save this to a file called secrets.tfvars and add that file to our gitignore

hcloud_token = "<secret_token>"

We’re not working with anyone else, so we don’t need to worry about a setting up a terraform remote store. Okay, so step one in the actual infrastructure is to setup a server. Let’s also create a firewall too, just to be safe.

resource "hcloud_firewall" "main_firewall" {
  name = "main_firewall"
  rule {
    direction = "in"
    protocol = "icmp"
    source_ips = [
      "0.0.0.0/0",
      "::/0"
    ]
  }
  
  rule {
    direction = "in"
    protocol  = "tcp"
    port      = "443"
    source_ips = [
      "0.0.0.0/0",
      "::/0"
    ]
  }
}

resource "hcloud_server" "blogHost" {
  name         = "blogHost"
  image        = "debian-13"
  server_type  = "cpx02"
  firewall_ids = [hcloud_firewall.main_firewall.id]
}

We’re opening the 443 port because we want to serve the blog to the public internet. We’re also opening the ICMP protocol to manage the server. We also want to enable SSH. But we do not want just anyone to be able to SSH into our server. Let’s create another firewall rule restricted to the IP of my machine and create an SSH key to use. We’ll generate an ssh key with the ssh-keygen command and then define a resource accordingly:

resource "hcloud_ssh_key" "main" {
  name       = "main-ssh-key"
  public_key = file("~/.ssh/blogServer.pub") 
}

And then we’ll use the built in http data source to retreive our current IP.

data "http" "myip" {
  url = "https://ifconfig.me/ip"
}

Then we’ll add a firewall rule to our main firewall preventing any access to port 22 except from our current IP.

  rule {
    direction = "in"
    protocol  = "tcp"
    port      = "22"
    source_ips = [data.http.myip.response_body]
  }

And now we can run an initial test with terraform apply -var-file secrets.tfvars Pro tip: use the yes command to auto-approve changes: yes yes | terraform apply ... One of the reasons I like terraform so much is how easy it makes setting up security rules. As we just saw, I can automatically add firewall rules using values and variables. We may eventually start working with IAM (identity and access managment) servers, and those configurations can get somewhat narly. But with terraform, it’s all covered.

Server Configuration

Alright let’s move on to actually setting up our server to host. For that, we’ll need a cloud init. The cloud init is a YAML file to specify the initial setup of our server. I’ve copied the cloud config file from https://community.hetzner.com/tutorials/basic-cloud-config/ Then we’ll replace ‘holu’ with my desired username: ‘austin’ And finally we’ll update the ssh key in the file: replace <public_ssh_key> with ${jsonencode(ssh_public_key)} and in terraform we’ll add

  user_data    = templatefile("cloud-init.yaml", {
    ssh_public_key = hcloud_ssh_key.main.public_key
  })

to the server resource. Now, we have a hardened SSH port. Let’s setup our caddy reverse proxy. We want to use a reverse proxy because we can set up multiple services, it will handle TLS, and it puts a layer of indirection between the open internet and our blog application. We’ll use Caddy as the reverse proxy because its easy to install and configure. This will listen on the open 443 port and send all requests to our blog server. Let’s start by creating a simple caddyFile:

austinhauter.me {
  reverse_proxy localhost:8080
}

That’s all, now we’ll add a caddy service config file so systemD will handle the caddy process.

[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

We’ll add these files to the cloud-config.yaml like so:

write_files:
  - path: /etc/caddy/Caddyfile
    content: ${jsonencode(caddyFile)}
  - path: /etc/systemd/system/caddy.service
    content: ${jsonencode(caddyService)}
    ...

We can also add Caddy as a package to install on initialization, so we don’t have to install it manually. And then in terraform we’ll add these files to the templatefile function:

  user_data    = templatefile("cloud-init.yaml", {
    ssh_public_key = hcloud_ssh_key.main.public_key
    caddyFile      = file("caddyFile")
    caddyService   = file("caddyService")
  })

While we’re at it, lets add a systemD service file for our blog application.

# blog.service
#
[Unit]
Description=HermesBlog
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=simple
User=blog
Group=blog
ExecStart=/home/austin/bin
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

Okay so the last thing is to deploy the binaries:

go build -o bin <path to project root>
scp -P 2222 -i ~/.ssh/blogServer ./bin austin@<server_ip>:/home/austin
scp -r -P 2222 -i ~/.ssh/blogServer ../content austin@<server_ip>:/home/austin
ssh <server_ip> -p 2222 -i ~/.ssh/blogServer 'sudo setfacl -m u:blog:--x /home/austin && sudo systemctl restart blog'

And that’s all done as well. The last thing we have to do is run terraform apply && ./deploy.sh Once those two tasks are done; our server should be setup and our blog running! Thanks for reading! We still have a lot of other things to work on with the blog, like CI/CD, cross-posting and maybe we’ll setup other applications as well! Stay tuned! :)