見出し画像

Ansible で BINDサーバを構築をしてみた

Ansibleとは

Ansibleは "構成管理ツール" と呼ばれ、サーバ構築、ネットワーク機器構築などを自動で行うことができます。 構成ツールには、Chef, Puppetなどもありますが、Ansible は記述が簡易的かつ、機器へのSSH設定なども記述する必要がない(エージェントレス)なので使いやすいと思います。複数のサーバ、ネットワーク機器の構築、設定変更も一度に行えるので、オペレーションコストを大きく削減できるとともに、人的要因な設定ミス等も防ぐことが可能なすげえ便利なやつです。

今回は、Ansibleを使って CentOS7 に BIND サーバ (named)の構築にチャレンジ。環境準備から Ansible Playbook の実行まで順にまとめてみました。
※ macbook (macOS 10.14.1) で実施。

(環境準備) Vagrantfileを用意

先ずは 以下の Vagrangfile で ansilbe vmを起動

1) vagrant, virtualboxのインストール

# Homebrew
brew cask install virtualbox vagrant

2) Vagrantfileを用意

root$ cat Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

require 'net/http'
require 'uri'

Vagrant.configure("2") do |config|
  config.vm.define "ansible" do |ansible|
  	$script = <<-'SCRIPT'
    yum update
    yum install epel-release ansible -y
    yum install samba-client vim git python-pip -y
    pip install pywinrm
    pip install pyvmomi
    pip install netaddr
    # Install Ansible 2.4.1 as yum repo has 2.3
    pip install git+git://github.com/ansible/ansible.git@stable-2.4
      SCRIPT

    ansible.vm.box = "bento/centos-7.2"
    ansible.vm.provision "shell" , inline: $script
    ansible.vm.hostname = "ansible"
    ansible.vm.network "private_network", ip: "192.168.30.10"
    ansible.vm.network "forwarded_port", guest: 80, host:8900

  end

config.push.define "local-exec" do |push|
  # Need to change NETCONF ...
  push.inline = <<-"SCRIPT"
  curl -u vagrant:vagrant http://localhost:2224/restconf/api/running/native/interface/GigabitEthernet/2 -X PUT -H 'Content-type: application/vnd.yang.data+json' --data @set_if.json
    SCRIPT
end

end

3) Vagrant upで起動
最初は image を引っ張ってくるので時間がかかるので注意。

root$ vagrant up ansible
Bringing machine 'ansible' up with 'virtualbox' provider...
==> ansible: Checking if box 'bento/centos-7.2' is up to date...
==> ansible: Clearing any previously set forwarded ports...
==> ansible: Clearing any previously set network interfaces...
==> ansible: Preparing network interfaces based on configuration...
    ansible: Adapter 1: nat
    ansible: Adapter 2: hostonly
==> ansible: Forwarding ports...
    ansible: 80 (guest) => 8900 (host) (adapter 1)
    ansible: 22 (guest) => 2222 (host) (adapter 1)
==> ansible: Booting VM...
==> ansible: Waiting for machine to boot. This may take a few minutes...
    ansible: SSH address: 127.0.0.1:2222
    ansible: SSH username: vagrant
    ansible: SSH auth method: private key
    ansible: Warning: Connection reset. Retrying...
    ansible: Warning: Remote connection disconnect. Retrying...
    ansible: Warning: Connection reset. Retrying...
    ansible: Warning: Remote connection disconnect. Retrying...
    ansible: Warning: Connection reset. Retrying...
    ansible: Warning: Remote connection disconnect. Retrying...
==> ansible: Machine booted and ready!
==> ansible: Checking for guest additions in VM...
    ansible: The guest additions on this VM do not match the installed version of
    ansible: VirtualBox! In most cases this is fine, but in rare cases it can
    ansible: prevent things such as shared folders from working properly. If you see
    ansible: shared folder errors, please make sure the guest additions within the
    ansible: virtual machine match the version of VirtualBox you have installed on
    ansible: your host and reload your VM.
    ansible:
    ansible: Guest Additions Version: 5.1.10
    ansible: VirtualBox Version: 5.2
==> ansible: Setting hostname...
==> ansible: Configuring and enabling network interfaces...
    ansible: SSH address: 127.0.0.1:2222
    ansible: SSH username: vagrant
    ansible: SSH auth method: private key
==> ansible: Rsyncing folder: /Users/git/ansible/workspace/ => /home/temp
==> ansible: Mounting shared folders...
    ansible: /vagrant => /Users/git/ansible
==> ansible: Machine already provisioned. Run `vagrant provision` or use the `--provision`
==> ansible: flag to force provisioning. Provisioners marked to run always will still run.
root$

4) ansible vm に ssh 接続
以下のように ansible vm が up (running)することを確認。

root$ vagrant status ansible
Current machine states:

ansible                   running (virtualbox)

The VM is running. To stop this VM, you can run `vagrant halt` to
shut it down forcefully, or you can run `vagrant suspend` to simply
suspend the virtual machine. In either case, to restart it again,
simply run `vagrant up`.

5) vagrant ssh ansible で 立ち上げた ansible vm に接続します。

root$ vagrant ssh ansible
Last login: Wed Mar 20 02:33:13 2019 from 10.0.2.2
[vagrant@ansible ~]$

Ansible Role Structure

ここから Ansible Playbookを作成していくのですが、Roleというモジュール構造で作っていきます。
この例では、bindというディレクトリ配下に tasks, handlers, templates, defaults, vars というサブディレクトリを配置してます。tasks の main.yml に作業プロセスを記述し、templates配下にはbind設定に必要な正引き、逆引きzone dbファイル、named.confファイル等を置いておきます。defaults配下には変数指定がない場合のデフォルト値を記述した main.yml、handlers配下にはtasks(main.yml)で notifyされるActionを記述、vars配下には 変数を指定した main.yml を配置しています。

.
├── bind
│   ├── defaults
│   │   └── main.yml
│   ├── handlers
│   │   └── main.yml
│   ├── tasks
│   │   └── main.yml
│   ├── templates
│   │   ├── db.forward.j2
│   │   ├── db.reverse.j2
│   │   ├── named.conf.j2
│   │   ├── named.conf.local.j2
│   │   └── resolv.j2
│   └── vars
│       └── main.yml
├── hosts
├── install_bind.yml
└── uninstall_bind.yml

tasks/main.yml

1つ1つの Role を作っていきます。先ずはtasks の設定です。この main.ymlには、bindをインストールしたり、ディレクトリを作成し、bindに必要なファイルの設定、Firewallの管理、などのainsilbeの根幹ともなる手順書を記述していきます。

[vagrant@ansible]$ cat bind/tasks/main.yml
#tasks file for Bind setup
- name: Set DNS Server to host1
  lineinfile:
   regexp: '8.8.8.8'
   insertafter: '^#'
   line: nameserver 8.8.8.8
   path: /etc/resolv.conf

- name: Install bind
  yum:
    pkg: bind
    state: installed

- name: Set hostname
  hostname:
    name: "{{ hostname }}.{{ domain }}"

- name: Set hostname fact
  set_fact:
    ansible_fqdn: "{{ hostname}}.{{domain }}"

- name: Copy named conf file
  template:
    src: named.conf.j2
    dest: /etc/named.conf
    owner: root
    group: named
    mode: 0660
  notify: Start named

- name: Make named directory
  file:
    path: /etc/named
    state: directory
    owner: root
    group: named
    mode: 0750

- name: Copy named conf local file
  template:
    src: named.conf.local.j2
    dest: /etc/named/named.conf.local
    owner: root
    group: named
    mode: 0640
  notify: Start named

- name: Make zones Directory
  file:
    path: /etc/named/zones
    state: directory
    owner: root
    group: named
    mode: 0750

- name: Copy forward file
  template:
    src: db.forward.j2
    dest: /etc/named/zones/db.forward
    owner: root
    group: named
    mode: 0640
  notify: Start named

- name: Copy reverse file
  template:
    src: db.reverse.j2
    dest: /etc/named/zones/db.reverse
    owner: root
    group: named
    mode: 0640
  notify: Start named

- name: check if firewalld is running
  command: systemctl is-active firewalld
  register: firewalld_result
  changed_when: False
  ignore_errors: True

- name: Open firewall port
  firewalld:
   service: dns
   permanent: true
   state: enabled
   immediate: yes
  when: firewalld_result.stdout == "active"

- name: resolv.conf replace
  template:
   src: resolv.j2
   dest: /etc/resolv.conf
   owner: root
   mode: 0640
  notify: Start named

defualts/main.yml

デフォルトの変数 (hosts file や vars/main.ymlで指定がない場合に使われる)
gather_fact で引っ張ってきた ansible_default_ipv4.network 変数をsplitで加工して reverse_domain subnet を設定しました。

---
# vars file for Bind setup
domain: example.com
hostname: "{{ ansible_hostname }}"
reverse_domain: 
 - reverse: "{{ ansible_default_ipv4.network.split('.')[0] }}.{{ ansible_default_ipv4.network.split('.')[1] }}.{{ ansible_default_ipv4.network.split('.')[2] }}.in-addr.arpa."

vars/main.yml

変数を設定していきます。reverse_domainとdns_record はリスト型で記述しています。

---

# ---- for bind server ---
#hostname の指定がない時は targetで既に設定されているhostnameを使用
hostname: ansible_test
#domain 必須パラメータ 指定がない場合は、example.com を使用
domain: example.com

# reverse_domain: x.x.x.in-addr.arpa. の記述 (zone毎にリストで記載)
# 指定がない時は、設定されている NICの subnetを用いて設定する
reverse_domain:
 - reverse: 1.168.192.in-addr.arpa.
 - reverse: 1.31.172.in-addr.arpa.

# dns_record: 正引き、逆引き用の DNS Record. 指定がない場合は、設定SKIP
# name/type/ipaddress は必須パラメータ
dns_record:
 - name: aaa
   type: A
   ipaddress: 192.168.1.110
 - name: bbb
   type: A
   ipaddress: 172.31.1.100

handlers/main.yml

tasks(main.yml)で notifyされるAction (ここでは Start named service )を設定

---
# handlers file for Bind setup
- name: Start named
  service:
    name: named
    state: started
  become: yes

templates/named.conf.j2

続いて templates 配下に named.conf.j2 を置きます。これは bindの conig fileである named.conf になります。
{{ ansible_default_ipv4.address }} は gather_facts で引っ張ってくる変数で、host の ip address が設定されます。Python の template ライブラリえある Jinja2 で書きます。

//
// named.conf
//
// Provided by Red Hat bind package to configure the ISC BIND named(8) DNS

// server as a caching only nameserver (as a localhost DNS resolver only).
//
// See /usr/share/doc/bind*/sample/ for example named configuration files.
//

include "/etc/rndc.key";

options {
	listen-on port 53 { 127.0.0.1; {{ ansible_default_ipv4.address }}; };
#	listen-on-v6 port 53 { ::1; };
	directory 	"/var/named";
	dump-file 	"/var/named/data/cache_dump.db";
	statistics-file "/var/named/data/named_stats.txt";
	memstatistics-file "/var/named/data/named_mem_stats.txt";
	allow-query     { any; };

	/*
	 - If you are building an AUTHORITATIVE DNS server, do NOT enable recursion.
	 - If you are building a RECURSIVE (caching) DNS server, you need to enable
	   recursion.
	 - If your recursive DNS server has a public IP address, you MUST enable access
	   control to limit queries to your legitimate users. Failing to do so will
	   cause your server to become part of large scale DNS amplification
	   attacks. Implementing BCP38 within your network would greatly
	   reduce such attack surface
	*/
	recursion yes;

	dnssec-enable yes;
	dnssec-validation yes;

        forwarders {
               8.8.8.8;
        };

	/* Path to ISC DLV key */
	bindkeys-file "/etc/named.iscdlv.key";

	managed-keys-directory "/var/named/dynamic";

	pid-file "/run/named/named.pid";
	session-keyfile "/run/named/session.key";
};

logging {
        channel default_debug {
                file "data/named.run";
                severity dynamic;
        };
};

zone "." IN {
	type hint;
	file "named.ca";
};

include "/etc/named/named.conf.local";

templates/named.conf.local.j2

BINDサーバの応答設定と、zoneファイルのパス指定。Reverseドメインは複数設定ある場合のために {% for item in reverse_domain %} でloop構造に。

zone "{{ domain }}" IN {
    type master;
    file "/etc/named/zones/db.forward";
    allow-update { key rndc-key; };
};

{% for item in reverse_domain %}
zone "{{ item.reverse }}" IN {
    type master;
    file "/etc/named/zones/db.reverse";
    allow-update {key rndc-key; };
};
{% endfor %}

controls {
     inet 127.0.0.1 port 953
         allow { 127.0.0.1; } keys { "rndc-key"; };
};

templates/db.forward.j2

正引きzoneファイル
{% if dns_record is defined %} を定義して、dns_record 変数がない場合は Record追加はskip

$TTL 604800	; 1 week
@	IN SOA	{{ ansible_fqdn }}. admin.{{ domain }}. (
				8          ; serial
				604800     ; refresh (1 week)
				86400      ; retry (1 day)
				2419200    ; expire (4 weeks)
				604800     ; minimum (1 weeks)
)
@			IN       NS	{{ ansible_fqdn }}.
{{hostname}}		IN	 A {{ ansible_default_ipv4.address }}
{% if dns_record is defined %}
{% for item in dns_record %}
{{ item.name }}			IN      {{ item.type }} {{ item.ipaddress }}
{% endfor %}
{% endif %}

templates/db.reverse.j2

逆引きzoneファイル
正引きと同様に、dns_record 変数がない場合は Record追加はskip

$TTL 604800	; 1 week
@	IN SOA	{{ ansible_fqdn }}. admin.{{ domain }}. (
				7          ; serial
				604800     ; refresh (1 week)
				86400      ; retry (1 day)
				2419200    ; expire (4 weeks)
				604800     ; minimum (1 week)
				)
	IN	NS	{{ ansible_fqdn }}.
{{ ansible_default_ipv4.address.split('.')[3] }}     IN	PTR {{ hostname }}.{{ ansible_default_ipv4.address.split('.')[2] }}.{{ ansible_default_ipv4.address.split('.')[1] }}.{{ ansible_default_ipv4.address.split('.')[0] }}.in-addr.arpa.
{% if dns_record is defined %}
{% for item in dns_record %}
{{ item.ipaddress.split('.')[3] }}	IN      PTR {{item.name}}.{{ item.ipaddress.split('.')[2] }}.{{item.ipaddress.split('.')[1]}}.{{item.ipaddress.split('.')[0]}}.in-addr.arpa.
{% endfor %}
{% endif %}

templates/db.resolv.j2

resolv.confの再定義

# Generated by NetworkManager
search example.com
nameserver 127.0.0.1
nameserver 8.8.8.8

hosts ファイルの作成

今回 Bind Serverを設定する対象 (centos 192.168.1.1)を指定して、root / passを指定する。

[vagrant@ansible]$ cat hosts
[centos]
192.168.1.1

[centos:vars]
ansible_user=root
ansible_password=p@ssw0rd

実行ファイルの作成 (install_bind.yml)

---
- name: Set up Bind
  hosts: centos
  gather_facts: true
  become: true

  roles:
    - bind

Playbookを実行してみる

[vagrant@ansible install_bind]$ ansible-playbook -i hosts install_bind.yml
/usr/lib/python2.7/site-packages/requests/__init__.py:80: RequestsDependencyWarning: urllib3 (1.22) or chardet (2.2.1) doesn't match a supported version!
  RequestsDependencyWarning)

PLAY [Set up Bind] ****************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************
ok: [192.168.1.1]

TASK [bind : Set DNS Server to host1] *********************************************************************************
ok: [192.168.1.1]

TASK [bind : Install bind] ********************************************************************************************
changed: [192.168.1.1]

TASK [bind : Set hostname] ********************************************************************************************
ok: [192.168.1.1]

TASK [bind : Set hostname fact] ***************************************************************************************
ok: [192.168.1.1]

TASK [bind : Copy named conf file] ************************************************************************************
changed: [192.168.1.1]

TASK [bind : Make named directory] ************************************************************************************
ok: [192.168.1.1]

TASK [bind : Copy named conf local file] ******************************************************************************
changed: [192.168.1.1]

TASK [bind : Make zones Directory] ************************************************************************************
changed: [192.168.1.1]

TASK [bind : Copy forward file] ***************************************************************************************
changed: [192.168.1.1]

TASK [bind : Copy reverse file] ***************************************************************************************
changed: [192.168.1.1]

TASK [bind : check if firewalld is running] ***************************************************************************
ok: [192.168.1.1]

TASK [bind : Open firewall port] **************************************************************************************
ok: [192.168.1.1]

TASK [bind : resolv.conf replace] *************************************************************************************
ok: [192.168.1.1]

RUNNING HANDLER [bind : Start named] **********************************************************************************
changed: [192.168.1.1]

PLAY RECAP ************************************************************************************************************
192.168.1.1              : ok=15   changed=7    unreachable=0    failed=0

実行結果の確認

Target Node (Centos) 側で named service が activeになっているかを確認。

[root@ansible-test named]# systemctl status named
● named.service - Berkeley Internet Name Domain (DNS)
   Loaded: loaded (/usr/lib/systemd/system/named.service; disabled; vendor preset: disabled)
   Active: active (running) since 土 2019-03-23 22:38:42 UTC; 3min 37s ago
  Process: 6240 ExecStart=/usr/sbin/named -u named -c ${NAMEDCONF} $OPTIONS (code=exited, status=0/SUCCESS)
  Process: 6238 ExecStartPre=/bin/bash -c if [ ! "$DISABLE_ZONE_CHECKING" == "yes" ]; then /usr/sbin/named-checkconf -z "$NAMEDCONF"; else echo "Checking of zone files is disabled"; fi (code=exited, status=0/SUCCESS)
 Main PID: 6242 (named)
   CGroup: /system.slice/named.service
           └─6242 /usr/sbin/named -u named -c /etc/named.conf

正引きzoneファイル

[root@ansible-test]# cat /etc/named/zones/db.forward
$TTL 604800	; 1 week
@	IN SOA	ansible-test.example.com. admin.example.com. (
				8          ; serial
				604800     ; refresh (1 week)
				86400      ; retry (1 day)
				2419200    ; expire (4 weeks)
				604800     ; minimum (1 weeks)
)
@			IN       NS	ansible-test.example.com.
ansible-test		IN	A 193.168.1.1
aaa			IN      A 192.168.1.110
bbb			IN      A 172.31.1.100

逆引きzoneファイル

[root@ansible-test]# cat /etc/named/zones/db.reverse
$TTL 604800	; 1 week
@	IN SOA	ansible-test.example.com. admin.example.com. (
				7          ; serial
				604800     ; refresh (1 week)
				86400      ; retry (1 day)
				2419200    ; expire (4 weeks)
				604800     ; minimum (1 week)
				)
	IN	NS	ansible-test.example.com.
162     IN	PTR ansible-test.1.168.192.in-addr.arpa.
110	IN      PTR aaa.1.168.192.in-addr.arpa.
100	IN      PTR bbb.1.31.172.in-addr.arpa.

こんな感じで一応動きました。Playbookを作り込むのは結構工数が掛かりますが、一度作ってしまうと本当に楽です。楽すぎて、linuxや自動化対象の設定コマンドなどを忘れてしまうのがAnsibleのこわいところでしょうか(汗。
 様々なシステム、テクノロジーが複雑化する中で、ヒューマン・エラーも最低限に抑え、シンプルだけどスケーラビリティが高い自動化テクノロジーは本当に重要ですね。「楽しよう」 という感覚をどんな作業に対しても持つことの大切さを改めて感じる今日このごろです。ではではまた。

今回の"note"を気に入って頂けましたら、是非サポートをお願いいたします!