これまで、Ansibleの構築とLinuxサーバへの接続確認まで実施した。今回は、Ansibleを使ってLinuxの初期設定と単体テストを行うPlaybookを作成した。作成する中で、Playbookのテクニックについても知見を得られたので、それも併せて記載する。
Linuxの初期設定とは、例えばsysstat
やtcpdump
などトラブルシューティング用の定番パッケージのインストール作業や、必要なサービスの起動・不要なサービスの停止作業が該当する。
今までのAnsible関連記事
- AnsibleコントロールノードをAlmaLinux上に構築する
- AnsibleからLinuxサーバに接続するための事前作業
- AnsibleでLinuxサーバの初期設定と単体テストを行う ←★本記事
- Ansibleのベストプラクティスのディレクトリ構成でPlaybookを実行する
- AnsibleからWindows Serverに接続するための事前作業
- AnsibleでWindows Serverの初期設定と単体テストを行う
- AnsibleでESXi上に仮想マシンを構築する
環境
コントロールノードとなる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_proxy とhttps_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_proxy とhttps_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 件のコメント:
コメントを投稿