cli53: Route 53 as a zone file you can validate before you upload
Command line tool for Amazon Route 53
At a glance
- What is it?
- cli53 is a small Go tool that treats a Route 53 hosted zone as a BIND zone file: you can validate a change offline, export a zone for backup, and import one back after seeing a dry run. The routing policies Route 53 is known for hang off ordinary flags, and the IAM policy section of the README contains the most useful warning in the whole project.
- Who is it for?
- Adopt cli53 when you inherited a DNS zone as text, or whenever you want every change to a zone to be reviewable as a file, and begin with `cli53 import --file zonefile.txt --replace --wait --dry-run example.com` before anything that writes. Scope the IAM policy yourself, because the shipped example grants `ChangeResourceRecordSets` and `DeleteHostedZone` against `Resource: "*"` and the README tells you to narrow that to specific hostedzone ARNs.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 157 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
DNS as text, in both directions
cli53 exists because a DNS zone is a text file and everything in Route 53 makes it awkward to keep it that way. The console is a form, the API returns JSON, and a zone that a hundred people can edit is a zone nobody can review. This tool's answer is to put BIND format back at the centre.
Import and export are the two operations that make the difference. Export writes a zone as a zone file, and the README says plainly it is for backup. Import reads one. Everything else in the tool is a convenience on top of those two.
So the workflow the tool enables is: change the file, validate the file, dry-run the import, then import. And there is a separate command for the middle step that needs no AWS call at all:
cli53 validate --file zonefile.txtThat is the feature that justifies the rest. A syntax error in a zone file is caught locally, before a `ChangeResourceRecordSets` call reaches a live zone that hundreds of people depend on.
Output formats matter for the same reason input formats do. Listing zones as JSON pipes into whatever you use to process them:
cli53 list -format json | jq .[].NameAnd export can be split so that data and diagnostics land in different places, which is what you want in a script:
cli53 export --full --debug example.com > example.com.txt 2> example.com.err.logThe `--full` flag exports fully-qualified domain names rather than bare prefixes, which matters because a zone file with relative names means something different depending on where it is imported, and `--debug` sends the SDK's own logging to standard error so the zone file on standard output stays clean.
Four import modes, and only one of them deletes
The import command is where a DNS tool can do serious damage, and this one has four modes with names that tell you what happens:
cli53 import --file zonefile.txt example.com
cli53 import --file zonefile.txt --replace --wait example.com
cli53 import --file zonefile.txt --replace --wait --dry-run example.com
cli53 import --file zonefile.txt --upsert example.comThe bare form adds the records in the file to the zone, which is the safe default and the one you want while you are still building the zone up. `--upsert` adds what is new and updates what has changed without deleting anything that is in the zone and missing from the file, which is what you want when the file is a partial description of a zone somebody else also edits.
`--replace` is the destructive one. It makes the zone match the file, which means records in the zone that are absent from the file get removed. That is the correct semantic for a zone file as a source of truth and a catastrophic one if the file is incomplete. Note that it is also the mode combined with `--wait`, which blocks until the change has propagated through Amazon's nameservers, so you learn about the result rather than assuming it.
And `--dry-run` makes any of them a report instead of a write. Combined with `--replace`, it is the combination worth memorising, because it is the one that answers the only question that matters before you touch a production zone: what exactly is about to be deleted.
So the design is defensible on its own terms. Deletion requires you to ask for deletion by name, the additive modes are the defaults, and there is a way to see the diff before committing. What the tool cannot do is make your zone file correct, and a zone file that was exported from a zone with dynamic records in it is not a complete description of that zone.
Routing policies as flags on a BIND-shaped record
Route 53's best feature is that a record is more than an address, and this tool exposes all of it without inventing a new syntax.
The record argument is a positional string that follows BIND convention, so zone-file muscle memory transfers directly:
cli53 rrcreate example.com 'www 60 A 192.168.0.1'Name, TTL, type, value. Everything specific to Route 53 hangs off a flag. Weighting uses an identifier and a weight, which is what Route 53 requires for weighted records because it keys the weight against a named instance:
cli53 rrcreate --identifier server1 --weight 10 example.com 'www A 192.168.0.1'Geolocation uses the same idea at three levels, continent, country and subdivision, with the identifier naming the group:
cli53 rrcreate -i Africa --continent-code AF example.com 'geo 300 IN A 127.0.0.1'
cli53 rrcreate -i California --country-code US --subdivision-code CA example.com 'geo 300 IN A 127.0.0.2'Failover pairs an identifier with a primary or secondary role and, on the primary, a health check identifier, which is the object you created in the Route 53 console or through the API beforehand. The pairing is the whole point of the feature: you have two servers, one health check, and DNS that sends traffic to whichever is up.
ALIAS records are where the syntax gets specific, and there are three forms. An alias to an Elastic Load Balancer names the load balancer DNS name and its hosted zone ID. An alias to another record uses the `$self` token to mean this zone. An alias to a CNAME names the target record.
Round robin needs no flags at all, which is the elegant case: pass two records with the same name and type and Route 53 distributes between them.
One detail that will bite somebody: trailing dots. For CNAME records a relative domain has no trailing dot and an absolute one does, so `login CNAME www` is correct and `mail CNAME ghs.googlehosted.com.` is correct. The README calls this out explicitly because the failure is silent, since a relative name that looks absolute will resolve somewhere you did not intend.
The credential surface reaches further than Route 53
This is the section of the README that deserves the most attention, and it starts with something ordinary: a standard shared credentials file, or the usual environment variables, then `--profile` or `AWS_PROFILE` to choose a set, and `--role-arn` to assume a role. You can combine the two, which is what you want in CI where the profile names the source account and the role names the target.
Then the IAM policy, and the warning attached to it. The shipped example has four statements. Two are account-level reads and two are creates, all on `Resource: "*"`. The other two are the management statements, and the README says this about them in so many words: they use `"Resource": "*"`, which lets cli53 modify and delete any hosted zone or reusable delegation set in the account.
That is the correct thing to warn about, because `DeleteHostedZone` on a wildcard is a very large hammer. The fix is given immediately, in the form of the ARN:
{
"Sid": "Cli53ManageZones",
"Effect": "Allow",
"Action": [
"route53:GetHostedZone",
"route53:ListResourceRecordSets",
"route53:ChangeResourceRecordSets",
"route53:DeleteHostedZone"
],
"Resource": "*"
}Replace that trailing `"Resource": "*"` with a list of the specific `hostedzone` and `delegationset` ARNs cli53 is allowed to touch, and the blast radius of a mistake becomes one zone. A warning block like this in a README is rare enough that it is worth crediting, because the same policy copied into a dozen accounts is exactly the kind of thing that is never revisited.
One thing the dependency list adds to the picture. A DNS tool has direct dependencies on the EC2 service client and on STS, not only on Route 53. STS explains itself, since assuming a role is what `--role-arn` does. EC2 is harder to justify from the command surface, and whatever the reason, it widens the API surface a cli53 credential can reach beyond the zone records you actually manage with it.
Alpine, the container image, and why binaries need a rebuild
There is a short note in the installation section that is more interesting than it looks.
The README says that on Alpine in Docker the pre-built binaries do not work, so you should either use Debian or build from source. That is a libc statement: the release binaries are not built in a way that runs on musl, so a slim Alpine base image, which is the obvious choice for a DNS utility, does not work with the artefact you just downloaded.
And the repository ships a Dockerfile anyway, which is the clue to how the author expects it to be used:
FROM alpine:latest
COPY cli53 /bin/cli53
RUN chmod +x /bin/cli53 && apk add --no-cache openssl ca-certificates
ENTRYPOINT ["cli53"]
CMD ["-v"]Three things to notice. It copies a `cli53` binary in from outside, which means it is a wrapper for a binary you produced, most likely with `go install`, rather than for one you downloaded. It installs OpenSSL and the certificate bundle at runtime, which tells you the AWS SDK still needs a certificate authority to talk to Route 53 over TLS. And the default command is `-v`, so starting the container with no arguments prints the version and exits, which is a sensible way to smoke-test an image without accidentally making a DNS change.
That also means the image is not for people who grabbed a release binary from GitHub, since the whole point of the file is that those binaries do not run here. Build it against Debian, or build the binary yourself and use this.
Two smaller details. The base image is `alpine:latest`, unpinned, which is a normal choice for a utility image and a supply-chain consideration for anything you put in a pipeline. And there is no user directive, so the process runs as root inside the container, which is unremarkable for a single-binary image that only makes API calls.
A small codebase with a test harness from the modules era
The repository root is unusually flat and unusually readable, because there are only a handful of Go files in it: `main.go`, `commands.go`, `lexer.go`, `bind.go`, `awsrr.go`, `formatters.go`, `instances.go` and `util.go`, with matching test files for the bind, formatters and util units.
Those names describe the whole design. A hand-written lexer for BIND zone files. A binder that turns those tokens into records. A module for Route 53's record types. A formatters module for the output formats. `instances.go` for the routing-policy identifiers. And `urfave/cli` v2 for the command line, which is the library rather than the standard library's flag package, which is why the flags read the way they do.
The hand-written lexer is worth a second look because `github.com/miekg/dns` is also a direct dependency. Two parsers of the same format coexist, which usually means one of them is there for validation or comparison against a reference implementation. Whichever it is, it means the tool's notion of what a valid zone file is is checked by code in this repository rather than delegated entirely.
The test infrastructure is where the project's age shows, and it shows in three specific places. The behaviour-driven testing library is pinned to a pseudo-version dated 2018. The assertion library is at version 1.4. The Makefile exports an environment variable named after a Go experiment that was removed years ago, and the integration test target builds a temporary GOPATH, symlinks the repository into it, and runs the suite from there, which is the pre-modules workflow preserved intact.
None of that is a functional problem. It does mean the test harness itself is the least maintained part of a project whose subject matter, DNS parsing, has not changed in thirty years. If you are contributing, that is where the effort should go.
Version 0.9, and where the real alternative begins
The release history is short and the gaps are uneven. v0.8.24 in April 2025, v0.8.25 in June 2025, then nothing until v0.9.0 on 2026-04-27, which is also the date of the last push. So the project crossed a version boundary after about a ten-month gap and has had no commit since.
That is not a reason to avoid it. cli53 is a small tool over an API that is itself extremely stable, and the fact that v0.9.0 exists means the maintainer chose to spend the gap on a breaking release rather than on features. The licence is MIT, the builds are produced with goreleaser from a config in the repository, and the README is current enough to document a security warning on the IAM policy, which is not something a dormant project bothers to do.
The alternatives are real, and the choice is about the unit of work. The AWS CLI reaches the same API with the same credentials and needs no install step, and for a one-off change it is the better answer. A declarative provider such as the Terraform Route 53 resource keeps DNS in the same state file as the rest of your infrastructure, which is what you want when a load balancer's name is computed rather than typed.
cli53's unit of work is the zone file, and that is the whole difference. If your DNS arrived as text, from a registrar export, from another provider or from a colleague, then a text-first tool means the migration is a file operation with a validation step and a dry run, and the review of that change is reading a diff. If your DNS is generated from infrastructure, none of that matters and a declarative provider is strictly better, because a zone file cannot be the output of a template that knows the load balancer's DNS name.
The credential story is common to all three, so the IAM scoping advice in the earlier section applies whichever you pick. That is the part most worth taking from this README, and it is the part that has nothing to do with cli53.
Editorial conclusion
Adopt cli53 when you inherited a DNS zone as text, or whenever you want every change to a zone to be reviewable as a file, and begin with `cli53 import --file zonefile.txt --replace --wait --dry-run example.com` before anything that writes. Scope the IAM policy yourself, because the shipped example grants `ChangeResourceRecordSets` and `DeleteHostedZone` against `Resource: "*"` and the README tells you to narrow that to specific hostedzone ARNs. Do not expect parity with the AWS CLI or a declarative provider, and pin v0.9.0 rather than tracking the branch, since the last push and the last release are both dated 2026-04-27 and the integration harness still assumes a pre-modules GOPATH.
Frequently asked questions
What does cli53 do for Amazon Route 53?
It manages Route 53 domains from the command line and imports and exports the BIND zone file format. You can create, delete and list hosted zones, create, update and delete individual records, create the AWS routing policies including failover, geolocation, latency, weighted and ALIAS records, and manage reusable delegation sets.
What is the difference between import, upsert, replace and dry-run in cli53?
A bare import adds the records in the file, `--upsert` adds and updates without deleting anything absent from the file, `--replace` makes the zone match the file and therefore removes records that are missing from it, and `--dry-run` turns any of them into a report instead of a write. The recommended first command is `--replace --wait --dry-run`.
How do I install cli53?
Download the binary from the GitHub releases page for Linux, Mac or Windows and move it into a directory on your PATH, or install it with `brew install cli53` on Mac. To build it yourself you need Go 1.21 or newer and run `go install github.com/barnybug/cli53/cmd/cli53@latest`, which puts the binary in your GOPATH bin directory.
Does cli53 work with AWS profiles and assumed roles?
Yes. Credentials come from the shared credentials file or from AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, and you select a set with `--profile` or AWS_PROFILE and assume a role with `--role-arn`. The two can be combined on one command, which is the usual shape for a pipeline that reads from one account and writes to another.
What IAM permissions does cli53 need, and how should I scope them?
The example policy needs list, create, get, change and delete actions across hosted zones and reusable delegation sets. The README warns that the management statements use `"Resource": "*"`, which lets cli53 modify or delete any zone in the account, and shows how to replace it with specific ARNs such as `arn:aws:route53:::hostedzone/Z456DEF`.
Can I run cli53 in a Docker container?
Yes, but not with the pre-built binaries. The README states they do not work on Alpine, so either use a Debian base or build from source. The repository's Dockerfile copies a cli53 binary you produced into an Alpine image, installs OpenSSL and the certificate bundle, and defaults to printing the version with `-v`.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/barnybug-cli53)