DNSCove

DNSCove guide

Updated

DNSCove as a CloudFormation custom resource

DNSCove isn't a native CloudFormation resource type, but you can manage DNSCove zones and records inside a CloudFormation stack with a Lambda-backed custom resource. CloudFormation calls your Lambda on Create/Update/Delete; the Lambda calls the DNSCove API. Records then follow the stack lifecycle — created with the stack, updated on change, and deleted on teardown.

CloudFormation stack ──lifecycle event──▶ Lambda (DNSCoveProvider)
                                             │  reads token from Secrets Manager
                                             ▼
                       POST /api/zones/{id}/rrsets   (Create/Update: UPSERT)
                       POST /api/zones/{id}/rrsets   (Delete: DELETE)
                       POST /api/zones               (zone provisioning)

Can CloudFormation manage DNSCove records?

Yes. Model DNSCove zones and records as Lambda-backed custom resources. The provider Lambda receives CloudFormation lifecycle events, calls the DNSCove JSON API, and reports success or failure back to the stack so DNS changes follow the same create, update, and delete workflow as the rest of your infrastructure.

1. Store a token in Secrets Manager

Use a tenant-admin token if the stack provisions zones; a zone-admin token is enough if it only manages records on one existing zone. Mint it once (see machine tokens in the API reference) and store the secret — never put the token literal in a template. Minting itself needs a credential too: either a signed-in console session (cookie + X-CSRF-Token, never a bearer header — see API.md's authentication section) or, as below, an existing tenant-admin scoped token used to bootstrap this one:

TOKEN=$(curl -sX POST https://api.dnscove.com/api/tokens \
  -H "Authorization: Bearer $BOOTSTRAP_TENANT_ADMIN_TOKEN" \
  -d '{"scope":"tenant-admin","name":"cloudformation","expires_in_days":90}' | jq -r .token)

aws secretsmanager create-secret --name dnscove/cfn-token --secret-string "$TOKEN"

2. The provider Lambda

A single Python 3.12 function (no external dependencies — urllib only) handles the custom-resource protocol for both records and zones. It reads the token from the DNSCOVE_TOKEN environment variable, which CloudFormation resolves from Secrets Manager via a dynamic reference.

# provider.py — DNSCove CloudFormation custom-resource provider
import json, os, urllib.request, urllib.error

API = os.environ.get("DNSCOVE_API", "https://api.dnscove.com")
TOKEN = os.environ["DNSCOVE_TOKEN"]

def _call(method, path, body=None):
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(API + path, data=data, method=method,
        headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req) as r:
            raw = r.read()
            return r.status, (json.loads(raw) if raw else {})
    except urllib.error.HTTPError as e:
        raise RuntimeError(f"{method} {path} -> {e.code}: {e.read().decode()}")

def _respond(event, context, status, physical_id, data=None, reason=""):
    body = json.dumps({
        "Status": status, "Reason": reason or f"see log stream {context.log_stream_name}",
        "PhysicalResourceId": physical_id, "StackId": event["StackId"],
        "RequestId": event["RequestId"], "LogicalResourceId": event["LogicalResourceId"],
        "Data": data or {},
    }).encode()
    req = urllib.request.Request(event["ResponseURL"], data=body, method="PUT",
        headers={"Content-Type": "", "Content-Length": str(len(body))})
    urllib.request.urlopen(req)

def handler(event, context):
    rtype = event["ResourceType"]           # Custom::DNSCoveRecord | Custom::DNSCoveZone
    req = event["RequestType"]               # Create | Update | Delete
    props = event.get("ResourceProperties", {})
    try:
        if rtype == "Custom::DNSCoveZone":
            physical, data = _zone(req, event, props)
        else:
            physical, data = _record(req, props)
        _respond(event, context, "SUCCESS", physical, data)
    except Exception as e:  # never leave a stack hanging — always respond
        physical = event.get("PhysicalResourceId", "dnscove-failed")
        _respond(event, context, "FAILED", physical, reason=str(e))

def _record(req, p):
    zone_id, name, rtype = p["ZoneId"], p["Name"], p["Type"]
    physical = f"{zone_id}/{name}/{rtype}"
    if req == "Delete":
        _call("POST", f"/api/zones/{zone_id}/rrsets",
              {"action": "DELETE", "name": name, "type": rtype,
               "records": p.get("Records", []), "ttl": int(p.get("TTL", 300))})
        return physical, {}
    # Create + Update are both UPSERT (idempotent). A changed Name/Type yields a
    # new PhysicalResourceId, so CloudFormation deletes the old record for us.
    _call("POST", f"/api/zones/{zone_id}/rrsets",
          {"action": "UPSERT", "name": name, "type": rtype,
           "ttl": int(p.get("TTL", 300)), "records": p["Records"]})
    return physical, {"ZoneId": zone_id}

def _zone(req, event, p):
    origin = p["Origin"]
    if req == "Create":
        _, z = _call("POST", "/api/zones", {"origin": origin})
        return z["id"], {"ZoneId": z["id"], "NS": ",".join(z.get("ns", []))}
    if req == "Delete":
        zone_id = event["PhysicalResourceId"]
        if zone_id.startswith("Z"):
            _call("DELETE", f"/api/zones/{zone_id}")
        return zone_id, {}
    # Update: origin is immutable; keep the existing zone.
    return event["PhysicalResourceId"], {"ZoneId": event["PhysicalResourceId"]}

Package and deploy it like any Lambda (inline ZipFile for small code, or an S3 artifact). Grant it secretsmanager:GetSecretValue only if you read the secret at runtime; the template below injects it at deploy time instead.

3. Use it in a template

Resources:
  DNSCoveProvider:
    Type: AWS::Lambda::Function
    Properties:
      Runtime: python3.12
      Handler: provider.handler
      Timeout: 30
      Role: !GetAtt ProviderRole.Arn
      Environment:
        Variables:
          # Resolve the secret at deploy time (never store the literal token).
          DNSCOVE_TOKEN: "{{resolve:secretsmanager:dnscove/cfn-token:SecretString}}"
      Code:
        S3Bucket: my-lambda-artifacts
        S3Key: dnscove/provider.zip

  # A record managed by the stack:
  WwwRecord:
    Type: Custom::DNSCoveRecord
    Properties:
      ServiceToken: !GetAtt DNSCoveProvider.Arn
      ZoneId: Z1a2b3c4d5e6f70
      Name: www.example.com.
      Type: A
      TTL: 300
      Records:
        - 192.0.2.10
        - 192.0.2.11

  # A zone provisioned by the stack (requires a tenant-admin token):
  AppZone:
    Type: Custom::DNSCoveZone
    Properties:
      ServiceToken: !GetAtt DNSCoveProvider.Arn
      Origin: app.example.com

Outputs:
  AppZoneNameservers:
    Value: !GetAtt AppZone.NS   # delegate the parent domain to these

Notes