Skip to main content

You are here

Ansible

Updating BIND DNS records using Ansible

This is a follow up to the post. Configure BIND to support DDNS updates
Now, that I'm able to dynamically update DNS records, this is where Ansible comes in. Ansible is hands down my favorite orchestration/automation tool. So I choose to use it to update my local DNS records going forward.

I'll be using the community.general.nsupdate module.

I structured my records on my nameserver's corresponding Ansible group_vars using the following structured:

all_dns_records:
  - zone: DNS-NAME
    records:
      - record: (@ for $ORIGIN or normal record name )
        ttl: TTL-VALUE
        state: (present or absent)
        type: DNS-TYPE
        value: VALUE-OF-DNS-RECORD

Example

---
all_dns_records:
  - zone: "rubyninja.org"
    records:
      - record: "@"
        ttl: "10800"
        state: "present"
        type: "A"
        value: "192.168.1.63"
      - record: "shit"
        ttl: "10800"
        state: "present"
        type: "A"
        value: "192.168.1.64"
  - zone: "alpha.org"
    records:
      - record: "@"
        ttl: "10800"
        state: "present"
        type: "A"
        value: "192.168.1.63"
      - record: "test"
        ttl: "10800"
        state: "present"
        type: "A"
        value: "192.168.1.64"
[...]

Deployment Ansible playbook:

---
- hosts: ns1.rubyninja.org
  pre_tasks:
    - name: Get algorithm from vault
      ansible.builtin.set_fact:
        vault_algorithm: "{{ lookup('community.general.hashi_vault', 'secret/systems/bind:algorithm') }}"
      delegate_to: localhost

    - name: Get rndckey from vault
      ansible.builtin.set_fact:
        vault_rndckey: "{{ lookup('community.general.hashi_vault', 'secret/systems/bind:rndckey') }}"
      delegate_to: localhost

  tasks:
    - name: Sync $ORIGIN records"
      community.general.nsupdate:
        key_name: "rndckey"
        key_secret: "{{ vault_rndckey }}"
        key_algorithm: "{{ vault_algorithm }}"
        server: "ns1.rubyninja.org"
        port: "53"
        protocol: "tcp"
        ttl: "{{ item.1.ttl }}"
        record: "{{ item.0.zone }}."
        state: "{{ item.1.state }}"
        type: "{{ item.1.type }}"
        value: "{{ item.1.value }}"
      when: item.1.record == "@"
      with_subelements:
        - "{{ all_dns_records }}"
        - records
      notify: Sync zone files
      delegate_to: localhost

    - name: Sync DNS records"
      community.general.nsupdate:
        key_name: "rndckey"
        key_secret: "{{ vault_rndckey }}"
        key_algorithm: "{{ vault_algorithm }}"
        server: "ns1.rubyninja.org"
        port: "53"
        protocol: "tcp"
        zone: "{{ item.0.zone }}"
        ttl: "{{ item.1.ttl }}"
        record: "{{ item.1.record }}"
        state: "{{ item.1.state }}"
        type: "{{ item.1.type }}"
        value: "{{ item.1.value }}"
      when: item.1.record != "@"
      with_subelements:
        - "{{ all_dns_records }}"
        - records
      notify: Sync zone files
      delegate_to: localhost

  post_tasks:
    - name: Check master config
      command: named-checkconf /var/named/chroot/etc/named.conf
      delegate_to: ns1.rubyninja.org
      changed_when: false

    - name: Check zone config
      command: "named-checkzone {{ item }} /var/named/chroot/etc/zones/db.{{ item }}"
      with_items:
        - "{{ all_dns_records | map(attribute='zone') | list }}"
      delegate_to: ns1.rubyninja.org
      changed_when: false

  handlers:
    - name: Sync zone files
      command: rndc -c /var/named/chroot/etc/rndc.conf sync -clean
      delegate_to: ns1.rubyninja.org

My DNS deployment a playbook breakdown:
1). Grabs the Dynamic DNS update keys from HashiCorp Vault
2). Syncs all of @ $ORIGIN records for all zone.
3). Syncs all of the records.
4). For good measure, but necessary: Checks named.conf file
5). For good measure, but necessary: Checks each individual zone file
6). Force dynamic changes to be applied to disk.

Given that in my environment I have roughly a couple of dozen DNS records, the structured for DNS records works in my environment. Thus said, my group_vars file with all my DNS records is almost 600 lines long. The playbook executing run takes around 1-2 minutes to complete. If I were to be in an environment where I had thousands of DNS records, the approached that I described here might not be the most efficient.

Awesome Applications: 

Annoying Ansible Gotcha

Ansible is by far my favorite Configuration Management tool, however it certainly has it's own unique quirks and annoyances. To start, I prefer the Ansible's YAML/Jinja approach instead of Puppet and Chef's own DSL custom configurations.

Today I ran into an interesting YAML parsing quirk. It turns out if you use colon ':' character inside a string anywhere in your playbooks, Ansible will fail to properly parse it.

Example playbook:

---
- hosts: 127.0.0.1
  tasks:
    - lineinfile: dest=/etc/sudoers regexp='^testuser ALL=' state=present line="testuser ALL=(ALL) NOPASSWD: TEST_PROGRAM" state=present

When running the playbook, triggers the following error:

ERROR! Syntax Error while loading YAML.


The error appears to have been in '/etc/ansible/one_off_playbooks/example.yml': line 4, column 104, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

  tasks:
    - lineinfile: dest=/etc/sudoers regexp='^testuser ALL=' state=present line="testuser ALL=(ALL) NOPASSWD: TEST_PROGRAM" state=present
                                                                                                       ^ here

Fix:
This is a known issue https://github.com/ansible/ansible/issues/1341 and the easiest work around for this, is to force the colon ':' character to be evaluated by the Jinja templating engine.

{{':'}}

The hilarious part of this, is that it doesn't look like this stupid quirk is going to be fixed.

Awesome Applications: 

Git Ansible Playbooks

One of the reasons I love Ansible over any other config management tool is because of its simplistic design and ease of use. It literally took me less than 15 minutes to write a set of playbooks to manage my local git server.

git_server_setup.yml - configures base server git repository configuration.

---
- hosts: git
  tasks:
  - name: Installing git package
    yum: name=git state=latest

  - name: Creating developers group
    group: name=developers state=present

  - name: Creating git user
    user: name=git group=developers home=/home/git shell=/sbin/nologin

  - name: Updating /home/git permissions
    file: path=/home/git mode=2770

create_git_user.yml - creates local system git user accounts.

---
- hosts: git
  tasks:

  - name: Creating new git user
    user: name={{ user_name }} password={{ user_password }} home=/home/git shell=/usr/bin/git-shell group=developers

  vars_prompt:
  - name: "user_name"
    prompt: "Enter a new git username"
    private: no

  - name: "user_password"
    prompt: "Enter a password for the new git user"
    private: yes
    encrypt: "sha512_crypt"
    confirm: yes
    salt_size: 7

create_git_repo.yml - creates an empty bare git repository.

---
- hosts: git
  vars:
    repo_name: www.alpha01.org

  tasks:
  - file: path=/home/git/{{ repo_name }} state=directory mode=2770

  - name: Creating {{ repo_name }} git repository
    command: git init --bare --shared /home/git/{{ repo_name }}

  - name: Updating repo permissions
    file: path=/home/git/{{ repo_name }} recurse=yes owner=git

Awesome Applications: 

System Update using Ansible

CentOS

ansible centosbox -m yum -a 'name=* state=latest'

Debian

ansible debianbox -m apt -a 'update_cache=yes name=* state=latest'

Linux: 

Awesome Applications: 

Premium Drupal Themes by Adaptivethemes