2022年7月30日土曜日

AnsibleでLinuxサーバの初期設定と単体テストを行う

これまで、Ansibleの構築とLinuxサーバへの接続確認まで実施した。今回は、Ansibleを使ってLinuxの初期設定と単体テストを行うPlaybookを作成した。作成する中で、Playbookのテクニックについても知見を得られたので、それも併せて記載する。

Linuxの初期設定とは、例えばsysstattcpdumpなどトラブルシューティング用の定番パッケージのインストール作業や、必要なサービスの起動・不要なサービスの停止作業が該当する。

今までのAnsible関連記事

環境

コントロールノードとなるOSは、AlmaLinuxを用いる。ただし、Red Hat系のディストリビューションであるCentOSやRocky Linuxなどでも同様の手順で構築できるだろう。

  • OS : AlmaLinux 8.5
  • Ansible : ansible [core 2.13.1]

前提として、SELinuxとfirewalldは停止しておく。

# systemctl stop firewalld
# systemctl disable firewalld
# sed -i "s/SELINUX=enforcing/SELINUX=disabled/g" /etc/selinux/config
# reboot

Playbookの処理概要

今回のPlaybookはLinux初期構築を行うものとなるが、大きく「構築パート」と「単体テストパート」に分けて作成している。

構築パートでAnsibleのモジュールを用いてLinuxサーバの各種設定やパッケージのインストールを行う。

単体テストパートでは、構築パートで実施した設定項目と一対一となるように設定確認を行う。しかし本来、Ansibleは冪等性 (処理を何回行っても必ず同じ結果となる性質) があるため、構築パートでAnsibleの処理が成功したことを確認できれば必ず正しい設定が反映されているはずであり、わざわざ単体テストは不要と感じるかもしれない。

ただし、再度設定値確認として単体テストを行うことで、Ansibleでは処理が成功していても何らかの理由で設定が反映されていない可能性を排除できることや、Ansibleの構築処理とは別に設定変更をさせずに単体テストを行うことができるため品質向上を図ることができると考える。

また、基本方針として、単体テストパートの処理を構築パートで利用したモジュールや処理と同じにしてしまうと、万が一設定変更処理自体に問題があった場合に検知ができない可能性があることから、単体テストパートでは別の処理内容 (別のコマンド) にて設定内容を確認をするようにPlaybookを作成した。

Playbookの処理詳細

Playbookのファイルの記載内容は大きく以下のように構成される。

設定項目 説明
hosts 実行対象のサーバを記載する。サーバのリストは別ファイルで作成し、そのファイルの中で実行する対象のサーバやグループを指定する。
vars 変数を記載する。ここで記載した変数は{{ 変数名 }}という形で参照できる。また、変数は配列して複数の要素を持たせることもできる。
tasks Ansibleで実行するメインの処理を記載する。
handlers Ansibleでメイン処理実行状況に応じて実行する事後処理を記載する。例えば、設定ファイル更新がされた場合 (実行結果がchangedの場合) は設定反映のためにサービス再起動を行うといった使い方をする。今回のPlaybookでは未使用となる。

Ansibleのベストプラクティスでは、これらのファイルを分割して決められたディレクトリに沿って配置するルールとなっているが、今回は入門編ということで、あえて1つのファイルにてPlaybookを作成した。

1. hosts : 実行対象サーバ

実行対象サーバを記載するファイルは、Playbookとは別にhostsというファイル名で作成した。なお、ファイル名はPlaybook実行時に指定できるため、任意の名前で作成して問題ない。

[]でグループ名を記載し、実行対象サーバを羅列する。記載方法はサーバのホスト名を記載し、実際に接続するIPアドレスをansible_hostの変数で記載するとわかりやすい。

hosts

[linux_servers]
server1 ansible_host=192.168.1.101
server2 ansible_host=192.168.1.102
server3 ansible_host=192.168.1.103

2. vars : 変数

Ansibleではvarsに、Playbookで利用する変数を記載する。今回は以下内容を変数として記載した。

変数名 内容
ansible_user Ansibleを実行するための実行対象サーバのユーザ名を記載。
proxy_env dnf実行時にプロキシサーバを指定し、インターネットからパッケージダウンロードをできるようにするため、環境変数http_proxyhttps_proxyを指定する。
install_package_list インストール対象のパッケージを一覧で指定。
disable_service_list 停止するサービスを一覧で指定。
enable_service_list 起動するサービスを一覧で指定。
ntp_server 実行対象に設定するNTPサーバを指定。
nmcli_ipv4_gateway 実行対象サーバに設定するデフォルトゲートウェイを指定。
nmcli_ipv4_dns 実行対象サーバに設定するDNSサーバを指定。
nmcli_ipv4_dns_search 実行対象サーバに設定するDNS検索パスを指定。
---
- name: Setup linux server
  gather_facts: no
  hosts: linux_servers

  vars:
    ansible_user: ansibleuser

    proxy_env:
      http_proxy: 192.168.1.1:8080
      https_proxy: 192.168.1.1:8080

    install_package_list:
      - open-vm-tools
      - rsyslog
      - chrony
      - sos
      - sysstat
      - tcpdump
      - cifs-utils
      - nfs-utils
      - bind-utils

    disable_service_list:
      - firewalld

    enable_service_list:
      - chronyd

    ntp_server: 192.168.1.2

    nmcli_ipv4_gateway: 192.168.1.254
    nmcli_ipv4_dns: "192.168.1.3 192.168.1.4"
    nmcli_ipv4_dns_search: test.local

~(以下略)~

3. tasks : 処理 (構築パート)

Playbookの処理はtasksに記載する。まず、実行対象サーバに設定を投入する構築パートの処理内容を以下に記載する。
※モジュール名はFQCNで記載すると長くなるため名前だけの記載にしているが、PlaybookにはFQCNで記載することを推奨する。

処理内容 モジュール 説明
タイムゾーン設定 timezone Asia/Tokyoに設定。
インタフェース設定 command nmcliコマンドを実行し、デフォルトゲートウェイ、DNSサーバ、DNS検索パスの設定を行う。設定反映のため、nmcliコマンドでインタフェースのUpを行う。なお、community.general.nmcliでも各種設定が可能ではあるが、私の環境では想定通りの動作がしないことがあったり、最終的に設定反映のためのインタフェースのUpは直接nmcliコマンドで実行する必要があることから、すべての設定作業をnmcliコマンドで直接実行する形式としている。
パッケージインストール dnf 変数install_package_listにて指定した複数のパッケージをdnfを用いてインストールする。その際にプロキシサーバ経由でインターネットアクセスができるよう環境変数http_proxyhttps_proxyを指定する。
サービス無効化service 変数disable_package_listで指定した複数のサービスを無効化する。
サービス有効化 service 変数enable_package_listで指定した複数のサービスを有効化する。
SELinux無効化selinux SELinuxを無効化し、再起動が必要な場合はOS再起動を行い設定を反映する。
時刻同期設定 (Chrony設定)lineinfile ChronyのNTP同期先を1つ設定する。具体的には、デフォルトで存在するserver句またはpool句を指定したserver句に置換する。設定後、Chronyのサービス再起動を行い設定を反映する。

setup_linux_server.yml (抜粋)

---
- name: Setup linux server
  gather_facts: no
  hosts: linux_servers

  vars:
   ~(中略)~
  tasks:
    # Build
    - name: Set timezone
      community.general.timezone:
        name: Asia/Tokyo
      become: true

    - name: Set network interface
      ansible.builtin.command: nmcli c mod ens192 {{ item }}
      loop:
        - connection.autoconnect yes
        - ipv4.gateway "{{ nmcli_ipv4_gateway }}"
        - ipv4.dns "{{ nmcli_ipv4_dns }}"
        - ipv4.dns-search "{{ nmcli_ipv4_dns_search }}"
      changed_when: false
      become: true

    - name: Restart network interface
      ansible.builtin.command: nmcli c up ens192
      changed_when: false
      become: true

    - name: Install packages
      ansible.builtin.dnf:
        name: "{{ package_install_list }}"
        state: present
      become: true
      environment: "{{ proxy_env }}"

    - name: Disable service
      ansible.builtin.service: name={{ item }} enabled=no state=stopped
      loop: "{{ service_disable_list }}"
      become: true

    - name: Enable service
      ansible.builtin.service: name={{ item }} enabled=yes state=started
      loop: "{{ service_enable_list }}"
      become: true

    - name: Disable SELinux
      ansible.posix.selinux:
        state: disabled
      become: true
      register: result

    - name: Reboot (when reboot required)
      ansible.builtin.reboot:
      async: 1
      poll: 0
      changed_when: false
      become: true
      when: result.reboot_required

    - name: Wait for reboot
      ansible.builtin.wait_for_connection:
        delay: 5
        timeout: 120
      when: result.reboot_required

    - name: Set chrony
      ansible.builtin.lineinfile:
        path: /etc/chrony.conf
        regexp: '^(pool|server)'
        line: 'server {{ ntp_server }} iburst'
      become: true

    - name: Restart chrony
      ansible.builtin.service: name=chronyd state=restarted
      become: true
      changed_when: false

~(以下略)~

4. tasks : 処理 (単体テストパート)

引き続きtasksの中の単体テストパートを記載する。
※モジュール名はFQCNで記載すると長くなるため名前だけの記載にしているが、PlaybookにはFQCNで記載することを推奨する。


処理内容 モジュール 説明
タイムゾーン確認 shell Asia/Tokyoに設定されていることをコマンドにて確認。
インタフェース設定確認 shell nmcliコマンドを実行し、デフォルトゲートウェイ、DNSサーバ、DNS検索パスの設定確認を行う。
パッケージインストール確認 shell 変数install_package_listにて指定した複数のパッケージをrpm -qコマンドを用いてインストールされていることを確認する。
サービス無効化確認 shell 変数disable_package_listで指定した複数のサービスをsystemctl is-enabledコマンドを用いて無効化されていることを確認する。なお、systemctlはサービスがdisabledの場合、返り値が1となるため、set -o pipefailは付与しない。
サービス有効化確認 shell 変数disable_package_listで指定した複数のサービスをsystemctl is-enabledコマンドを用いて有効化されていることを確認する。なお、systemctlはサービスがdisabledの場合、返り値が1となるため、set -o pipefailは付与しない。
SELinux確認 command SELinuxが無効化されていることをコマンドにて確認する。
時刻同期設定確認 (Chrony設定確認) shell ChronyのNTP同期先をに対して時刻同期が成功していることを確認する。

setup_linux_server.yml (抜粋)

---
- name: Setup linux server
  gather_facts: no
  hosts: linux_servers

  vars:
   ~(中略)~
  tasks:
   ~(中略)~

    # Unit test
    - name: Test timezone
      ansible.builtin.shell: set -o pipefail && timedatectl status | grep 'Asia/Tokyo'
      changed_when: false

    - name: Test network interface
      ansible.builtin.shell: set -o pipefail && nmcli c show ens192 | grep -E {{ item }}
      loop:
        - '^connection.autoconnect.*yes'
        - '^ipv4.addresses.*{{ ansible_host }}'
        - '^ipv4.gateway.*{{ nmcli_ipv4_gateway }}'
        - '^ipv4.dns.*$(echo "{{ nmcli_ipv4_dns }}" | tr " " ",")'
        - '^ipv4.dns-search.*{{ nmcli_ipv4_dns_search }}'
      changed_when: false
      register: result
      environment:
        LANG: C

    - name: Test install packages
      ansible.builtin.shell: rpm -q "{{ item }}"
      loop: "{{ package_install_list }}"
      changed_when: false
      register: result
      tags: skip_ansible_lint

    - name: Test disable service
      ansible.builtin.shell: systemctl is-enabled "{{ item }}" | grep 'disabled'
      loop: "{{ service_disable_list }}"
      changed_when: false
      register: result
      tags: skip_ansible_lint

    - name: Test enable service
      ansible.builtin.shell: systemctl is-enabled "{{ item }}" | grep 'enabled'
      loop: "{{ service_enable_list }}"
      changed_when: false
      register: result
      tags: skip_ansible_lint

    - name: Test SELinux
      ansible.builtin.command: getenforce
      changed_when: false
      register: result
      failed_when: result.stdout != "Disabled"

    - name: Test chrony
      ansible.builtin.shell: set -o pipefail && chronyc -n sources | grep -E '\^\* {{ ntp_server }}' | wc -l
      changed_when: false
      register: result
      until: result.stdout | int == 1
      retries: 3
      delay: 5

実行結果

実際にPlaybookを実行してみよう。failed=0であれば、問題なく処理が成功している。

# ansible-playbook -i hosts -l server1 setup_linux_server.yml
PLAY [Setup linux server] ***************************************************************************************************

TASK [Set timezone] *********************************************************************************************************
changed: [server1]

TASK [Set network interface] ************************************************************************************************
ok: [server1] => (item=connection.autoconnect yes)
ok: [server1] => (item=ipv4.gateway "192.168.1.254")
ok: [server1] => (item=ipv4.dns "192.168.1.4")
ok: [server1] => (item=ipv4.dns-search "test.local")

TASK [Restart network interface] ********************************************************************************************
ok: [server1]

TASK [Install packages] *****************************************************************************************************
changed: [server1]

TASK [Disable service] ******************************************************************************************************
changed: [server1] => (item=firewalld)

TASK [Enable service] *******************************************************************************************************
ok: [server1] => (item=chronyd)

TASK [Disable SELinux] ******************************************************************************************************
[WARNING]: SELinux state temporarily changed from 'enforcing' to 'permissive'. State change will take effect next reboot.
changed: [server1]

TASK [Reboot (when reboot required)] ****************************************************************************************
ok: [server1]

TASK [Wait for reboot] ******************************************************************************************************
ok: [server1]

TASK [Set chrony] ***********************************************************************************************************
changed: [server1]

TASK [Restart chrony] *******************************************************************************************************
ok: [server1]

TASK [Test timezone] ********************************************************************************************************
ok: [server1]

TASK [Test network interface] ***********************************************************************************************
ok: [server1] => (item=^connection.autoconnect.*yes)
ok: [server1] => (item=^ipv4.addresses.*192.168.1.101)
ok: [server1] => (item=^ipv4.gateway.*192.168.1.254)
ok: [server1] => (item=^ipv4.dns.*$(echo "192.168.1.4" | tr " " ","))
ok: [server1] => (item=^ipv4.dns-search.*test.local)

TASK [Test SELinux] *********************************************************************************************************
ok: [server1]

TASK [Test install packages] ************************************************************************************************
ok: [server1] => (item=open-vm-tools)
ok: [server1] => (item=rsyslog)
ok: [server1] => (item=chrony)
ok: [server1] => (item=sos)
ok: [server1] => (item=sysstat)
ok: [server1] => (item=tcpdump)
ok: [server1] => (item=cifs-utils)
ok: [server1] => (item=nfs-utils)
ok: [server1] => (item=bind-utils)

TASK [Test disable service] *************************************************************************************************
ok: [server1] => (item=firewalld)

TASK [Test enable service] **************************************************************************************************
ok: [server1] => (item=chronyd)

TASK [Test chrony] **********************************************************************************************************
ok: [server1]

PLAY RECAP ******************************************************************************************************************
server1                  : ok=18   changed=5    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

以上で、AnsibleによるLinuxの初期設定と単体テストを実現することができた。今回は1つのPlaybookにてすべての処理を実行したが、前述したとおりAnsibleのベストプラクティスでは、varsやtasksなどのファイルを分割してPlaybookを構成する。

次回は、Ansibleのベストプラクティスのディレクトリ構成にてPlaybookを実行してみたいと思う。

0 件のコメント:

コメントを投稿

人気の投稿