Manage DNS through a git-based workflow
Ruby Shell
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.

README.md

Record Store

Record Store is a tool to manage DNS through a git-based workflow.

Getting Started

record-store apply                     # Applies the DNS changes
record-store assert_empty_diff         # Asserts there is no divergence between DynECT & the zone files
record-store diff                      # Displays the DNS differences between the zone files in this repo and production
record-store download -n, --name=NAME  # Downloads all records from zone and creates YAML zone definition in zones/ e.g. record-store download --name=sho...
record-store freeze                    # Freezes all zones under management to prevent manual edits
record-store help [COMMAND]            # Describe available commands or one specific command
record-store list                      # Lists out records in YAML zonefiles
record-store secrets                   # Decrypts DynECT credentials
record-store sort -n, --name=NAME      # Sorts the zonefile alphabetically e.g. record-store sort --name=shopify.io
record-store thaw                      # Thaws all zones under management to allow manual edits
record-store validate_change_size      # Validates no more then particular limit of DNS records are removed per zone at a time
record-store validate_initial_state    # Validates state hasn't diverged since the last deploy
record-store validate_records          # Validates that all DNS records have valid definitions

Providers

Below is the list of DNS providers supported by Record Store. PRs adding more are welcome.

DNSimple

Record Store uses DNSimple's v2 API. To use DNSimple, you'll need to add the primary user's account_id and api_token to secrets.json.

DynECT

In order to use DynECT, you'll need to create a user that has the correct read and write permissions. Add the user's username & password (i.e. API password) as well as your DynECT customer name to secrets.json.

Design

The DynECT provider uses DynECT's DNS API to sync the YAML zone files. DynECT uses an update/publish cycle in their API which means no changes take place until POSTing to the publish endpoint. This allows us to handle all failures by discarding the changes we attempted to create.

The DynECT zones managed by Record Store are frozen in DynECT (frozen zones cannot be changed); the deploy process will thaw them so it can make the necessary changes, and refreeze once the deploy process has completed.

DynECT permissions

The permissions required are broken into 2 groups:

  • READ: RecordGet, ZoneGet
  • WRITE: RecordAdd, RecordDelete, ZonePublish, ZoneDiscardChangeset, ZoneFreeze, ZoneThaw, ZoneAddNode, ZoneRemoveNode

All CI validations only require READ permissions; deploying requires a user with READ and WRITE permissions.

In addition, the AliasService permission is required to be able to read or write ALIAS records on DynECT.

For a breakdown of what each permission allows read through DynECT's permissions guide.


Architecture

All CLI commands are defined in lib/record_store/cli.rb with Thor.

Zones and Records

The Zone and Record models are representations of their DNS equivalents. Both have validations to ensure configurations are RFC compliant. These are specified using ActiveModel::Validations.

Most CLI interactions are through the Zone model.

Providers

In order to be provider agnostic, Record Store encapsulates all provider interactions in the Provider model and its children. A provider is initialized for each zone.

Changeset

Changesets are how Record Store knows what updates to make. A Changeset is generated by comparing the current records in a zone with the desired final state. A Changeset is composed of one or more Changeset::Change. Each Change is either an addition, removal, or update. Since the ID of records aren't specified in zone files, FQDNs are used to dedup when records can be updated or when new ones need to be created.

When running bin/record-store apply, a Changeset is generated by comparing the current records in a zone's YAML file with the records the provider defines. A zone's YAML file is always considered the primary source of truth.


Development

To get started developing on Record Store, run bin/setup. This will create a development directory, dev/, that mimics what a production directory managing DNS records using Record Store would look like. Use it as a sandbox when developing Record Store. You can use bin/console to get play with the dev data, or you can cd into dev/ and use bin/record-store to test out the CLI.

Adding new Providers

To add a new Provider, create a class inheriting Provider in lib/record_store/provider/. The DynECT provider is good to use as a reference implementation.

Note: there's no need to wrap Provider#apply_changeset unless it's necessary to do something before/after making changes to a zone.

Provider API interactions are tested with VCR. To generate the fixtures, update test/dummy/secrets.json with valid credentials, run the test suite, and change the values back to stub credentials.

Important: be sure to filter sensitive data from the fixtures or you're going to have a bad time.

Outline of Provider:

class Provider
  # Downloads all the records from the provider.
  #
  # Returns: an array of `Record` for each record in the provider's zone
  def retrieve_current_records
  end

  # Returns an array of the zones managed by provider as strings
  def zones
  end

  ######## NOTE ########
  # The following methods only need to be implemented if the provider supports the ability to
  # lock/unlock changes to zones.
  ######################

  # Lock the ability to make any changes to the zone without unlocking it first. It is expected
  # this call modifies external state.
  def freeze_zone
  end

  # Unlocks the zone to allow making changes (see `Provider#freeze_zone`).
  def thaw
  end

  private

  ######## NOTE ########
  # The following methods only need to be implemented if you are using the base provider's
  # implementation of apply_changeset to manage the contents of the changeset (or transaction).
  ######################

  # Creates a new record to the zone. It is expected this call modifies external state.
  #
  # Arguments:
  # record - a kind of `Record`
  def add(record)
  end

  # Deletes an existing record from the zone. It is expected this call modifies external state.
  #
  # Arguments:
  # record - a kind of `Record`
  def remove(record)
  end

  # Updates an existing record in the zone. It is expected this call modifies external state.
  #
  # Arguments:
  # id - provider specific ID of record to update
  # record - a kind of `Record` which the record with `id` should be updated to
  def update(id, record)
  end
end

Provider-Specific Records

For provider-specific records (e.g. ALIAS), create the record model in lib/record_store/record as any other record. In the provider, extend self.record_types and append the custom record types to the Set returned by Provider.record_types (e.g. DNSimple.record_types).

Secrets

When adding a new provider, be sure to update the secrets.json in template/secrets.json and test/dummy/secrets.json with the new provider and required fields for the API to work.

Test Changes on Providers

In order to test changes on providers, you're going to need to update dev/secrets.json with credentials. Note: make sure the credentials are for test zone(s) as the changes specified in the directory will be applied.

Acknowledgements

Big thanks to @pjb3 for graciously letting us use the record_store gem namespace.