Managing your DNS records with Terraform

At my first FOSDEM, I went together with a co-worker to see a talk from Matteo Valentini regarding DNS and how to manage your records with a CI/CD pipeline.

He showed us octoDNS a python tool from GitHub able to sync your local configuration with your DNS records managed at any thinkable cloud provider.

Until last weekend I was still using octoDNS to automatically manage my DNS on Azure through a CI/CD pipeline run with drone.

But I decided to switch to a different solution consisting of terraform and Digitalocean while keeping the pipeline on a self hosted drone server.

Setting up your project

I created a main.tf file and a separate file for every single DNS zone I want to manage:

etc.

The contents of main.tf will describe the provider we want to use (in this case digitalocean/digitalocean), our API Token as variable and the remote backend s3 which will be a space/bucket on Digitalocean. We will use the backend to save our terraform.tfstate.

terraform {
  required_providers {
    digitalocean = {
      source = "digitalocean/digitalocean"
      version = "2.0.1"
    }
  }

  # DigitalOcean uses the S3 spec.
  backend "s3" {
    bucket = "mybucketname"
    # filename to use for saving our tfstate
    key    = "terraform.tfstate" 
    # depends where you are setting up the space (fra1/ams1 etc..)
    endpoint = "https://ams1.digitaloceanspaces.com" 
    # DO uses the S3 format
    # eu-west-1 is used to pass TF validation
    region = "eu-west-1" 
    # Deactivate a few checks as TF will attempt these against AWS
    skip_credentials_validation = true
    skip_metadata_api_check = true
  }
}

# our digitalocean api token
variable "do_token" {} 

provider "digitalocean" {
  token = var.do_token 
}

The domain zone file

Our domain zone file will be kept very simple:

resource "digitalocean_domain" "examplecom" {
   name = "example.com"
   ip_address = "1.2.3.4" # default @ record
}

resource "digitalocean_record" "examplecom-mail" {
  domain = digitalocean_domain.examplecom.name
  type = "A"
  name = "mail"
  value = "1.2.3.5" # mail.example.com resolves to this IP
}

resource "digitalocean_record" "examplecom-mx" {
  domain = digitalocean_domain.examplecom.name
  type = "MX"
  name = "@"
  priority = 10
  value = "mail.example.com." # MX record
}

resource "digitalocean_record" "examplecom-www" {
  domain = digitalocean_domain.examplecom.name
  type = "CNAME"
  name = "www"
  value = "@" # CNAME record www.example.com > example.com
}

resource "digitalocean_record" "examplecom-txt-keybase" {
  domain = digitalocean_domain.examplecom.name
  type = "TXT"
  name = "_keybase"
  value = "keybase-site-verification=SECRETCODE" # keybase verification TXT record
}


resource "digitalocean_record" "examplecom-srv-imap-tcp" {
  domain = digitalocean_domain.examplecom.name
  type = "SRV"
  name = "_imap._tcp"
  value = "mail.example.com." # SRV record for imap
  port = "143"
  priority = 0
  weight = 1
 }

Init/plan/apply

What we now need is a init, plan & apply to finish this up. But first we will have to export our secrets


export TF_VAR_do_token=SECRET_API_TOKEN
# has nothing to do with AWS, it's still Digitalocean, but terraform's s3 backend reads this
export AWS_ACCESS_KEY_ID=KEY_ID_FOR_ACCESS_TO_DO_SPACE 
export AWS_SECRET_ACCESS_KEY=ACCES_KEY_FOR_ACCESS_TO_DO_SPACE 

terraform init
Initializing the backend...

Initializing provider plugins...
- Using previously-installed digitalocean/digitalocean v2.0.1

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

terraform plan
[...]
digitalocean_record.examplecom-www: Refreshing state... 
digitalocean_record.examplecom-mail: Refreshing state...
digitalocean_record.examplecom-mx: Refreshing state... 
[...]
Plan: 6 to add, 0 to change, 0 to destroy.


terraform apply # confirm with yes

After applying the changes, please check that your terraform.tfstate has been uploaded to the Digitalocean space and check if the DNS is actually working:

host example.com
example.com has address 1.2.3.4

Automating it

Let’s automate this by running a pipeline with drone. You can of course use any other CI/CD pipeline tooling you want to. For the main step in the pipeline we’ll be using the hashicorp/terraform container image.

Example .drone.yml:

kind: pipeline
type: docker
name: dns

steps:
  - name: terraform
    image: hashicorp/terraform:0.13.4
    commands:
      - terraform init
      - terraform plan
      - terraform apply -auto-approve
    # keep your secrets secret and not inside GIT!
    environment:
      TF_VAR_do_token:
        from_secret: tf_var_do_token
      AWS_SECRET_ACCESS_KEY:
        from_secret: aws_secret_access_key
      AWS_ACCESS_KEY_ID:
        from_secret: aws_access_key_id
    when:
      branch: master

This way any time you push a new change to your master branch, the pipeline will take care of the rest.

And thanks to the remote backend being configured, you’ll be able to also apply your changes manually, from any device.