Migrating from Zerigo to Rackspace Cloud DNS using Libcloud

In this blog post I’m going to describe how to migrate from Zerigo DNS to Rackspace Cloud DNS using a ~80 lines long Python script which utilizes Libcloud.

Background and Motivation

In September of the last year, I wrote how to export a Libcloud zone to the BIND zone format and use the BIND zone file to migrate between DNS providers.

At that time, my motivation for migrating away from Zerigo was mostly fueled by a very unreliable service which was a consequence of DDoS attacks and less than ideal service architecture.

I have a paid Zerigo plan, so back then, I only migrated the most important domains to a different provider. Not long after I have done this, Zerigo announced that they have partnered with Akamai and that going forward, they will outsource running of the DNS infrastructure to Akami and as such, the service should be way more stable and reliable.

I thought great, I won’t need to migrate rest of the domains away, but an unplesant surprise came earlier this month, when Zerigo announced pricing changes (see 1, 2, 3 & 4).

Previously, I have paid 19$ years per year, but with a new plan which matches my current one, I would need to pay 25$ per month. That’s with an existing customer loyalty discount. New customers will need to pay 38$ per month (what a great deal, instead of paying 24 times more, now I need to pay just 15 times more!). Yes, you have read this correctly, that’s more than one order of magniture per year more than I used to pay before.

I honestly don’t mind paying for a great software and services and I wouldn’t mind paying a little more if the service improved, but that kind or price increase is simply too much. That is especially true, because all of the ~15 domains that I still have at Zerigo are used to host non-profit and community websites and paying 25$ per month is simply too much.

Why Rackspace Cloud DNS?

Disclamer: I used to work at Rackspace, but I don’t work there anymore and I’m not affiliated with them in any way.

Before I dive further, lets have a look at why you might want to use Rackspace Cloud DNS.

The main reason for me to migrate to Rackspace is that they have a decent API, they are supported in Libcloud and best of all, the service is totally free for the existing cloud servers customers. On top of that, the service is supposed to use Anycast.

All of that made it a good fit for hosting my non-profit domains there.

I also need to add that I haven’t used the service a lot before, so I can’t really talk much about the service relablitity at this point. Only time and monitoring will tell how reliable the service really is.

Migrating from Zerigo DNS to Rackspace Cloud DNS using Libcloud

Instead of using Libcloud’s export to BIND zone file functionality, this script works by talking directly to both of the provider APIs.

The reason for that is that this approach is more robust and makes performing partial migrations and synchronizations easier. On top of that it also works with other providers which don’t support importing a BIND zone file.

It’s also important to note that the script relies on some Libcloud fixes which are currently only available in trunk. As such, you should use pip to install latest version from Git inside a virtual environment:

pip install -e git+https://github.com/apache/libcloud.git@trunk#egg=libcloud

After you have done this, you can use the script bellow to migrate all of your zones from Zerigo to Rackspace:

import hashlib

from libcloud.dns.types import Provider, RecordType
from libcloud.dns.providers import get_driver

ZERIGO_USERNAME = ''
ZERIGO_API_KEY = ''

RACKSPACE_USERNAME = ''
RACKSPACE_API_KEY = ''

CONTACT_EMAIL = ''  # Rackspace requires a valid email for every domain

ZONE_TTL = 30 * 60  # Default zone TTL (in seconds) which should be used
MIN_TTL = 300  # Minim TTL supported by the target provider
IGNORED_RECORD_TYPES = [RecordType.NS, RecordType.PTR]

source_cls = get_driver(Provider.ZERIGO)(ZERIGO_USERNAME, ZERIGO_API_KEY)
destination_cls = get_driver(Provider.RACKSPACE)(RACKSPACE_USERNAME,
                                                 RACKSPACE_API_KEY)


def get_record_hash(record):
    """
    Return a hash for the provided record. This is used to determine if the
    record already exists.
    """
    record_hash = hashlib.md5('%s-%s-%s' % (record.name, record.type,
                                            record.data)).hexdigest()
    return record_hash

source_zones = source_cls.list_zones()
destination_zones = destination_cls.list_zones()

destination_domains = [zone.domain for zone in destination_zones]

# 1. Create zones
for zone in source_zones:
    if zone.domain in destination_domains:
        print('Zone "%s" already exists, skipping...' % (zone.domain))
        continue

    extra = {'email': CONTACT_EMAIL}

    print('Creating zone: %s' % (zone.domain))
    destination_cls.create_zone(domain=zone.domain, ttl=ZONE_TTL,
                                extra=extra)

destination_zones = destination_cls.list_zones()

supported_record_type = destination_cls.list_record_types()

# 2. Create records
for source_zone in source_zones:
    destination_zone = [zone for zone in destination_zones
                        if zone.domain == source_zone.domain][0]

    source_records = source_zone.list_records()
    destination_records = destination_zone.list_records()

    for source_record in source_records:
        # Rackspace doesn't have a special SPF record type
        if source_record.type == RecordType.SPF:
            source_record.type = RecordType.TXT

        record_hash = get_record_hash(source_record)
        destination_record_hashes = [get_record_hash(record) for record
                                     in destination_records]

        if source_record.name:
            fqdn = '%s.%s' % (source_record.name, source_zone.domain)
        else:
            fqdn = source_zone.domain

        if record_hash in destination_record_hashes:
            print('Record "%s" already exists, skipping...' % (fqdn))
            continue

        if source_record.type in IGNORED_RECORD_TYPES:
            print(('Encountered ignored record type (type=%s,name=%s) '
                  'skipping...') % (source_record.type, fqdn))
            continue

        if type not in supported_record_type:
            print(('Encountered unsupported record type (type=%s,name=%s)'
                  ', skipping...') % (source_record.type, fqdn))
            continue

        extra = {}

        ttl = source_record.extra.get('ttl', None)
        priority = source_record.extra.get('priority', None)

        if ttl:
            if ttl < MIN_TTL:
                ttl = MIN_TTL
            extra['ttl'] = ttl

        if priority:
            extra['priority'] = priority

        name = source_record.name
        type = source_record.type
        data = source_record.data

        print('Creating a record: %s' % (fqdn))
        destination_zone.create_record(name=name, type=type, data=data,
                                       extra=extra)

Before proceeeding it’s worth knowing that there are some differences between the providers and some limitations you should be aware of:

  • Zerigo supports more record types. If you use more advanced record types which are not supported by Rackspace, then Rackspace might not be a good fit for you.
  • Rackspace only allows you to create PTR records for resources (cloud servers & load balancers) which are hosted in their data centers.
  • Rackspace doesn’t support SPF record type. This is not a big deal since this record type has been deprecated anyway and TXT can be used instead. This script transparently handled remapping of SPF to TXT for you.
  • Minimum supported TTL by Zerigo is 180 seconds and the minimum supported TTL by Rackspace is 300 seconds. If during the migration the script encounteres a TTL smaller than 300 seconds, it simply uses the smallest possible TTL which is 300 seconds.

To use it, simply plug in your API credentials and run it:

python migrate_dns_providers.py
Zerigo control panel.

If the script for some reason fails half-way through (bad connectivity, API issues, etc.), it’s safe to run it again since all the operations are idempotent.

Rackspace Cloud DNS control panel after the migration.

After you have run the script, you should check if everything looks OK and if it does, you can go ahead and change the DNS records for your domains to point to the Rackspace Cloud DNS servers (dns1.stabletransit.com & dns2.stabletransit.com).