DNSCove

DNSCove guide

DNSCove in Terraform / OpenTofu

There is no first-class terraform-provider-dnscove yet (it's on the roadmap), but DNSCove's REST API maps cleanly onto two well-supported patterns you can use today:

Object Pattern Why
Zones Mastercard/restapi provider zones are create/read/delete REST resources with a stable id
Records null_resource + local-exec the rrsets endpoint is UPSERT/DELETE by (name,type), not id-based CRUD

Both authenticate with a tenant-admin token (needed to create zones) or a zone-admin token (records on one existing zone). Mint one as described under machine tokens in the API reference and pass it as a sensitive variable — never commit the literal.

variable "dnscove_token" {
  type      = string
  sensitive = true # export TF_VAR_dnscove_token=dnsc_… (or pull from Vault/SSM)
}

variable "dnscove_api" {
  type    = string
  default = "https://api.dnscove.com"
}

Zones — via the restapi provider

terraform {
  required_providers {
    restapi = { source = "Mastercard/restapi", version = "~> 2.0" }
  }
}

provider "restapi" {
  uri                  = var.dnscove_api
  write_returns_object = true
  id_attribute         = "id" # DNSCove returns the zone id as `id`
  headers = {
    "Authorization" = "Bearer ${var.dnscove_token}"
    "Content-Type"  = "application/json"
  }
}

resource "restapi_object" "app_zone" {
  path = "/api/zones"
  data = jsonencode({ origin = "app.example.com" })

  # Origin is immutable in DNSCove; a change means a new zone.
  lifecycle { ignore_changes = [data] }
}

output "app_zone_id" {
  value = restapi_object.app_zone.id
}

output "app_zone_ns" {
  value = jsondecode(restapi_object.app_zone.api_response).ns
}

restapi_object POSTs to /api/zones on create, reads GET /api/zones/{id}, and DELETE /api/zones/{id} on destroy — exactly the DNSCove zone lifecycle.

Records — a reusable null_resource module

The rrsets endpoint UPSERTs on apply and DELETEs on destroy. This module wraps that so a record follows Terraform state and is torn down cleanly. Save as modules/dnscove_record/main.tf:

variable "api" { type = string }
variable "token" {
  type      = string
  sensitive = true
}
variable "zone_id" { type = string }
variable "name" { type = string }
variable "type" { type = string }
variable "ttl" {
  type    = number
  default = 300
}
variable "records" { type = list(string) }

locals {
  base = "${var.api}/api/zones/${var.zone_id}/rrsets"
  auth = "Authorization: Bearer ${var.token}"
}

resource "null_resource" "record" {
  # Re-run UPSERT whenever the record's content changes.
  triggers = {
    zone_id = var.zone_id
    name    = var.name
    type    = var.type
    ttl     = var.ttl
    records = join(",", var.records)
    api     = var.api
    # token intentionally excluded from triggers so rotation doesn't churn records
  }

  provisioner "local-exec" {
    command = <<-EOT
      curl -fsS -X POST '${local.base}' -H '${local.auth}' \
        -d '${jsonencode({ action = "UPSERT", name = var.name, type = var.type, ttl = var.ttl, records = var.records })}'
    EOT
  }

  # On destroy, DELETE the set. self.triggers keeps the values available even
  # though the vars are gone at destroy time.
  provisioner "local-exec" {
    when    = destroy
    command = <<-EOT
      curl -fsS -X POST '${self.triggers.api}/api/zones/${self.triggers.zone_id}/rrsets' \
        -H 'Authorization: Bearer ${var.token}' \
        -d '{"action":"DELETE","name":"${self.triggers.name}","type":"${self.triggers.type}"}'
    EOT
  }
}

Terraform forbids referencing non-self variables in a destroy provisioner, so identifying fields are read from self.triggers. The token is supplied through the module var at destroy time.

Use it:

module "www" {
  source  = "./modules/dnscove_record"
  api     = var.dnscove_api
  token   = var.dnscove_token
  zone_id = restapi_object.app_zone.id
  name    = "www.app.example.com."
  type    = "A"
  records = ["192.0.2.10", "192.0.2.11"]
}

Making changes live

Record UPSERTs are committed to the control-plane and reach the edge on the next publish. To publish from Terraform after a record change, add a publish step that depends on your records:

resource "null_resource" "publish" {
  triggers = { records = module.www.id } # re-publish when records change
  provisioner "local-exec" {
    command = "curl -fsS -X POST '${var.dnscove_api}/api/publish' -H 'Authorization: Bearer ${var.dnscove_token}'"
  }
  depends_on = [module.www]
}

Notes