The goal here is to see how to add and update A records on our cloudflare managed domain using shell scripting. Eventually we'll wire this, the hetzner command line client, and some mrsk inspired scripts to get a machine up and running quickly.

First some basic unix tools to make it easier to manupulate json:

1
  brew install jq jo

Get the Key

We'll need an API key, so lets go get that.

  1. Go to the api-tokens page in your profile.
  2. Create a Token
  3. Select Edit zone DNS template
  4. I'm limiting it to a specific zone.
  5. Copy and store the resulting token.

Test the token

1
2
3
  curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
       -H "Authorization: Bearer ${CF_TOKEN}" \
       -H "Content-Type:application/json" | jq
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "result": {
    "id": "726441c7a18f40e6ed5444a4635effd2",
    "status": "active"
  },
  "success": true,
  "errors": [],
  "messages": [
    {
      "code": 10000,
      "message": "This API Token is valid and active",
      "type": null
    }
  ]
}

Listing out zones

1
2
3
4
5
  curl -X GET \
       https://api.cloudflare.com/client/v4/zones \
       -H "Authorization: Bearer ${CF_TOKEN}" \
       -H 'Content-Type: application/json' | \
       jq -r '{"result"}[] | .[] | .id + " " + .name + " " + .status'
1
7b3f2ff4b23ab88aa09326590263561b willschenk.com active

Finding your zone identifier

I'm using willschenk.com here but feel free to adjust.

1
2
3
4
5
6
  export ZONE=willschenk.com

  curl -X GET \
       "https://api.cloudflare.com/client/v4/zones?name=${ZONE}&status=active" \
       -H "Authorization: Bearer ${CF_TOKEN}" \
       -H "Content-Type:application/json" | jq -r '{"result"}[] | .[0] | .id'

And in my case, we get:

1
7b3f2ff4b23ab88aa09326590263561b

Listing out your records

1
2
3
4
5
6
7
  export ZONE_ID=7b3f2ff4b23ab88aa09326590263561b

  curl -X GET \
       https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records \
       -H 'Content-Type: application/json' \
       -H  "Authorization: Bearer ${CF_TOKEN}" | \
      jq -r '{"result"}[] | .[] | "| \(.id) | \(.type) | \(.name) | \(.content) |"'

Which yields something like:

7d2953…CNAMEsummarizer.willschenk.comtwilight-butterfly-9726.fly.dev
e45be8…CNAMEwillschenk.comwschenk.github.io
31fb96…CNAMEwww.willschenk.comwschenk.github.io
f278db…TXTdev.willschenk.comgoogle-si…
e35e0f…TXTwillschenk.comALIAS for wschenk.github.io
d93a74…TXTwillschenk.comgoogle-si…

Looking up a record

1
2
3
4
5
6
7
8
  export ZONE_ID=7b3f2ff4b23ab88aa09326590263561b
  export RECORD=apple.willschenk.com

  curl -X GET \
       https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${RECORD} \
       -H 'Content-Type: application/json' \
       -H  "Authorization: Bearer ${CF_TOKEN}" | \
      jq -r '{"result"}[] | .[0] | .id'

And if you have a result, it will be something like:

1
425a21b7d74e958230f9f50d82adc836

Adding a record

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  export ZONE=willschenk.com
  export ZONE_ID=7b3f2ff4b23ab88aa09326590263561b
  export NAME=apple
  export IP=65.108.63.49

  jo type=A name=${NAME}.${ZONE} content=${IP} ttl=1 proxied=false | \
      curl -X POST \
           https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records \
           -d @- \
           -H 'Content-Type: application/json' \
           -H  "Authorization: Bearer ${CF_TOKEN}" | \
      jq

If the name is new, you'll see something like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
  "result": {
    "id": "3a7b82024e407dc58814b89fa5f45795",
    "zone_id": "7b3f2ff4b23ab88aa09326590263561b",
    "zone_name": "willschenk.com",
    "name": "apple.willschenk.com",
    "type": "A",
    "content": "65.108.63.49",
    "proxiable": true,
    "proxied": false,
    "ttl": 1,
    "locked": false,
    "meta": {
      "auto_added": false,
      "managed_by_apps": false,
      "managed_by_argo_tunnel": false,
      "source": "primary"
    },
    "comment": null,
    "tags": [],
    "created_on": "2023-06-20T17:20:39.753139Z",
    "modified_on": "2023-06-20T17:20:39.753139Z"
  },
  "success": true,
  "errors": [],
  "messages": []
}

You'll get an error if something already exists, so lets wire it all together.

Updating or adding a record

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  export ZONE_ID=7b3f2ff4b23ab88aa09326590263561b
  export RECORD=apple.willschenk.com
  export IP=65.108.63.49

  RECORD_ID=$(
      curl -X GET \
           https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${RECORD} \
           -H 'Content-Type: application/json' \
           -H  "Authorization: Bearer ${CF_TOKEN}" | \
          jq -r '{"result"}[] | .[0] | .id')

  if [[ $RECORD_ID == 'null' ]]; then
      echo Creating ${RECORD}
      jo type=A name=${RECORD} content=${IP} ttl=1 proxied=false | \
          curl -X POST \
               https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records \
               -d @- \
               -H 'Content-Type: application/json' \
               -H  "Authorization: Bearer ${CF_TOKEN}" | \
          jq
  else
      echo Updating $RECORD_ID
      jo type=A name=${RECORD} content=${IP} ttl=1 proxied=false | \
          curl -X PUT \
               "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
               -d @- \
               -H 'Content-Type: application/json' \
               -H  "Authorization: Bearer ${CF_TOKEN}" | \
          jq
  fi

This first checks to see if there's already an A record, and if not it created one. Otherwise, it updates it.

Either way it returns the latest info.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Creating apple.willschenk.com
{
  "result": {
    "id": "425a21b7d74e958230f9f50d82adc836",
    "zone_id": "7b3f2ff4b23ab88aa09326590263561b",
    "zone_name": "willschenk.com",
    "name": "apple.willschenk.com",
    "type": "A",
    "content": "65.108.63.49",
    "proxiable": true,
    "proxied": false,
    "ttl": 1,
    "locked": false,
    "meta": {
      "auto_added": false,
      "managed_by_apps": false,
      "managed_by_argo_tunnel": false,
      "source": "primary"
    },
    "comment": null,
    "tags": [],
    "created_on": "2023-06-20T17:30:53.819641Z",
    "modified_on": "2023-06-20T17:30:53.819641Z"
  },
  "success": true,
  "errors": [],
  "messages": []
}

Delete a record

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
  export ZONE_ID=7b3f2ff4b23ab88aa09326590263561b
  export RECORD=apple.willschenk.com

  RECORD_ID=$(
      curl -X GET \
           https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?name=${RECORD} \
           -H 'Content-Type: application/json' \
           -H  "Authorization: Bearer ${CF_TOKEN}" | \
          jq -r '{"result"}[] | .[0] | .id')

  if [[ $RECORD_ID == 'null' ]]; then
      echo ${RECORD} doesn\'t exist!
  else
      echo Deleting $RECORD_ID

      curl -X DELETE \
           "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records/${RECORD_ID}" \
           -d @- \
           -H 'Content-Type: application/json' \
           -H  "Authorization: Bearer ${CF_TOKEN}" | \
          jq
  fi
1
2
3
4
5
6
7
8
9
Deleting 5682275094b8e7726688afb8290b9372
{
  "result": {
    "id": "5682275094b8e7726688afb8290b9372"
  },
  "success": true,
  "errors": [],
  "messages": []
}

Previously

Controlling Hetzner with CLI Simple wrapper scripts

2023-06-19

Next

Style your RSS feed

2023-06-20

labnotes

Previously

Controlling Hetzner with CLI Simple wrapper scripts

2023-06-19

Next

Deploying a private docker registry Deployment testing

2023-06-21