Hetzner DNS via Terraform

01. July 2020 - Hetzner just launched Free DNS Management solution.

Here is my tutorial, how to setup your DNS records programmatically via Terraform

Install Terraform Provider for Hetzner DNS - https://github.com/timohirt/terraform-provider-hetznerdns

You can use insekticid/k8s-upgrade:latest

My Docker image with Terraform + Provider for Hetzner DNS plugin https://hub.docker.com/r/insekticid/k8s-upgrade

docker run --rm -it --entrypoint=sh -v "$(pwd):/app" insektici
d/k8s-upgrade

Terraform directory structure

/app/
├── modules/
│   └── records/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf (empty)
├── .env
├── .env.dist
├── docker-compose.yml
├── main.tf
└── terraform.tfvars
  • mkdir modules/records
  • cd modules/records
  • touch main.tf outputs.tf variables.tf

modules/records/main.tf

resource "hetznerdns_zone" "zone1" {
    name = var.domain
    ttl  = 60
}

resource "hetznerdns_record" "root" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "@"
    value   = var.ip_v4
    type    = "A"
    ttl     = 60
}

resource "hetznerdns_record" "root6" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "@"
    value   = var.ip_v6
    type    = "AAAA"
    ttl     = 60

    count   = var.ip_v6 != "" ? 1 : 0
}

resource "hetznerdns_record" "www" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "www"
    value   = "${var.domain}."
    type    = "CNAME"
    ttl     = 3600
}

resource "hetznerdns_record" "all" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "*"
    value   = "${var.domain}."
    type    = "CNAME"
    ttl     = 3600
}

resource "hetznerdns_record" "mx" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "@"
    value   = "1 aspmx.l.google.com."
    type    = "MX"
    ttl     = 3600
}

resource "hetznerdns_record" "mxalt1" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "@"
    value   = "5 alt1.aspmx.l.google.com."
    type    = "MX"
    ttl     = 3600
}

resource "hetznerdns_record" "mxalt2" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "@"
    value   = "5 alt2.aspmx.l.google.com."
    type    = "MX"
    ttl     = 3600
}

resource "hetznerdns_record" "mxalt3" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "@"
    value   = "10 alt3.aspmx.l.google.com."
    type    = "MX"
    ttl     = 3600
}

resource "hetznerdns_record" "mxalt4" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "@"
    value   = "10 alt4.aspmx.l.google.com."
    type    = "MX"
    ttl     = 3600
}

resource "hetznerdns_record" "txtgoogle" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "@"
    value   = var.google_site_verification
    type    = "TXT"
    ttl     = 3600
    
    count   = var.google_site_verification != "" ? 1 : 0
}

resource "hetznerdns_record" "txtdkim" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "google._domainkey"
    value   = var.dkim
    type    = "TXT"
    ttl     = 3600
    
    count   = var.dkim != "" ? 1 : 0
}

resource "hetznerdns_record" "txtspf" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "@"
    value   = var.spf
    type    = "TXT"
    ttl     = 3600
    
    count   = var.spf != "" ? 1 : 0
}

resource "hetznerdns_record" "txtdmarc" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "_dmarc"
    value   = replace(var.dmarc, "DOMAIN", hetznerdns_zone.zone1.name)
    type    = "TXT"
    ttl     = 3600
    
    count   = var.dmarc != "" ? 1 : 0
}

resource "hetznerdns_record" "txtbrave" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "@"
    value   = var.brave_site_verification
    type    = "TXT"
    ttl     = 3600
    
    count   = var.brave_site_verification != "" ? 1 : 0
}

resource "hetznerdns_record" "gsuite_mail" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "mail"
    value   = "ghs.google.com."
    type    = "CNAME"
    ttl     = 3600
    
    count   = var.gsuite != "" ? 1 : 0
}

resource "hetznerdns_record" "gsuite_calendar" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "calendar"
    value   = "ghs.google.com."
    type    = "CNAME"
    ttl     = 3600
    
    count   = var.gsuite != "" ? 1 : 0
}

resource "hetznerdns_record" "gsuite_docs" {
    zone_id = hetznerdns_zone.zone1.id
    name    = "docs"
    value   = "ghs.google.com."
    type    = "CNAME"
    ttl     = 3600
    
    count   = var.gsuite != "" ? 1 : 0
}

modules/records/variables.tf

variable "domain" {}
variable "google_site_verification" {
  default  = ""
}
variable "brave_site_verification" {
  default  = ""
}
variable "bing_site_verification" {
  default  = ""
}
variable "dkim" {
  default  = ""
}

variable "spf" {
  default  = ""
}

variable "dmarc" {
  default  = ""
}

variable "gsuite" {
  default  = "1"
}

variable "ip_v4" {}
variable "ip_v6" {
  default  = ""
}

main.tf

# you can save your state in S3 bucket instead of local file

#terraform {
#  backend "s3" {
#    bucket     = "terraform-shared-state-xyz"
#    region     = "eu-west-1"
#    key        = "dns/terraform.tfstate"
#  }
#}

# you can get another data from you remote state S3 bucket
# e.g. your cluster LB ipv4/ipv6

#data "terraform_remote_state" "cluster" {
#  backend = "s3"

#  config {
#    bucket     = "terraform-shared-state-xyz"
#    region     = "eu-west-1"
#    key        = "cluster/terraform.tfstate"
#  }
#}

locals {
  #ip_v4  = data.terraform_remote_state.cluster.lb_ip_v4
  #ip_v6  = data.terraform_remote_state.cluster.lb_ip_v6

  ip_v4   = "yourIPv4"
  ip_v6   = "yourIPv6"
  spf     = "v=spf1 include:_spf.google.com ~all"

  #do not modify DOMAIN string! it will be replaced with domain name automatically
  dmarc   = "v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@DOMAIN; pct=20; sp=none"
}

#variable "hetznerdns_token" {}

provider "hetznerdns" {
  #apitoken = var.hetznerdns_token
}

module "example_com_records" {
  source  = "./modules/records"
  
  domain  = "example.com"
  ip_v4   = local.ip_v4
  ip_v6   = local.ip_v6
  dkim    = "v=DKIM1; k=rsa; p=YOURRSAKEY"
  spf     = local.spf
  dmarc   = local.dmarc

  google_site_verification = "google-site-verification=YOURGOOGLEVERIFYKEY"
  bing_site_verification   = "YOURBINVERIFYKEY"
  brave_site_verification  = "brave-ledger-verification=YOURBRAVEVERIFYKEY"
}

# example 2 
# without IPv6
# only google site verification
# without gsuite subdomains - default is ON

module "example2_com_records" {
  source  = "./modules/records"
  
  domain  = "example2.com"
  ip_v4   = local.ip_v4
  gsuite  = ""

  google_site_verification = "google-site-verification=YOURGOOGLEVERIFYKEY"
}

terraform.tfvars

#hetznerdns_token = yourhetznerdnsapitoken

You can use HETZNER_DNS_API_TOKEN=yourhetznerdnsapitoken

Now you can run terraform init, terraform plan, terraform apply -auto-approve

Now change DNS records for your domain in your domain registrator console

Change NS property of your domain to Hetzner name servers

Edit: use docker-compose.yaml

create new file docker-compose.yaml in your terraform directory

version: '2.2'

services:
    terraform:
        image: insekticid/k8s-upgrade
        volumes:
           - ./:/terraform/
        env_file: .env
        working_dir: /terraform

create .env.dist

AWS_ACCESS_KEY_ID="anaccesskey"
AWS_SECRET_ACCESS_KEY="asecretkey"
AWS_DEFAULT_REGION="eu-west-1"
HETZNER_DNS_API_TOKEN="andnsapitoken"

Copy env dist to .env file and edit you secrets

cp .env.dist .env and 

Now you can run

  • docker-compose run --rm terraform init
  • docker-compose run --rm terraform plan
  • docker-compose run --rm terraform apply