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-
selfvariables in adestroyprovisioner, so identifying fields are read fromself.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
- Least privilege — a records-only stack needs a
zone-admintoken; only zone creation needstenant-admin. - State & secrets — the token is marked
sensitive, but it still lands in Terraform state. Use a remote backend with encryption and a short-lived token (expires_in_days), and rotate. - OpenTofu — everything here works unchanged under
tofu. - Native provider — a first-class provider would give real diffs and drift
detection for records; until then the
null_resourcemodule is the supported pattern. Track the roadmap in the repo.