Ubuntu

Ubuntu24.04 Ansible

Ubuntu

仮想サーバー7台を運用しているのだが、色々とバラバラになっているのが気になっていた。
Geminiさんとの会話の中で、「Ansibleで設定を一括管理するのか、個別管理なのか」という問いかけがあり、Ansibleって何?と聞いていくうちに、これはやっておかなければ…と考えた。



ここから広告
広告
広告ここまで


今回もGeminiさんと相談しながら試し、その結果を整理した。

これはいつか自分で使いそうだ…と思ったので、サンプルを以下に置いている。
gitea.rohhie.net / rohhie / ansible-sample

管理する側の準備

Ansibleで管理する対象がUbuntuだけ、という前提であれば、

  • やることリストを作る
  • 管理対象にSSHで接続してそれを実行

と考えて良さそうなので、まずは、それができる環境を作る。

インストール

ansibleをインストールする。python関連のパッケージが一緒にインストールされる、約300MB。

$ sudo apt install ansible

SSH接続で利用する鍵の作成

ansibleはssh接続をして様々処理をしていく。

ansible-adminというユーザー用に鍵を作る。パスフレーズは入力しなかった。

$ ssh-keygen -t ed25519 -C "ansible-admin-key" -f ~/.ssh/id_ed25519_ansible
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/rohhie/.ssh/id_ed25519_ansible
Your public key has been saved in /home/rohhie/.ssh/id_ed25519_ansible.pub
The key fingerprint is:
SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ansible-admin-key
The key's randomart image is:
+--[ED25519 256]--+
| |
| |
| |
| |
| |
| |
| |
| |
| |
+----[SHA256]-----+

$ ll .ssh/id_ed25519_ansible*
-rw------- 1 rohhie rohhie 411 Jul 17 06:24 .ssh/id_ed25519_ansible
-rw-r--r-- 1 rohhie rohhie 99 Jul 17 06:24 .ssh/id_ed25519_ansible.pub

※マスクしている。

管理される側に置くので、公開鍵を見ておく。

$ cat .ssh/id_ed25519_ansible.pub
ssh-ed25519 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ansible-admin-key

※全然問題ないとは思うけど、念のためマスクしている。

設定ファイル

ansible.cfg

様々設定ができるけれど、今必要と思ったものだけを書いておく。
Ansible Community Documentation / Ansible Configuration Settings

[defaults]
# インベントリファイルのデフォルトを指定
inventory = production.ini

# Ansible Vault のパスワードが記載されたファイルパスを指定
vault_password_file = .ansible_password

# メッセージの表示方法をJSON形式ではなくYAML形式に変える
stdout_callback = yaml

# SSH接続時のホストキーチェック(初回のyes/no確認)をスキップしたい場合は有効化
host_key_checking = False

# ファクトの自動収集
# fact_caching_timeout = 0 は無期限
#gathering = implicit
gathering = smart
fact_caching = jsonfile
fact_caching_prefix =
fact_caching_timeout = 86400
fact_caching_connection = .ansible_cache
#gathering = explicit

[ssh_connection]
# SSH接続を維持してタスク実行を高速化
pipelining = True

production.ini

インベントリーで、管理対象のホストと、ホストとやりとりする方法を書いておく。

[all:vars]
ansible_user=ansible-admin
ansible_ssh_private_key_file=~/.ssh/id_ed25519_ansible

fetch_dir="./artifacts/fetched"

[all_servers]
server1 ansible_host=sv1.hogeserver.hogeddns.jp
server2 ansible_host=sv2
router ansible_host=192.168.110.10 ansible_port=22222

※ansible_hostは名前解決ができるなら、どんな指定の仕方でも良かった。

.ansible_password

管理対象のファイルにはパスワードファイルも含まれる。
Git(Gitea)で履歴を管理しようと思っているので、パスワードファイルの暗号化を考えている。

そのためにはVaultという仕組みを使うのだけれど、このVaultで使用するパスワードを書いておく。
これは生パスワードで、人間が覚えるヤツ。

password

.gitignore

Gitでパスワードファイルを無視するように設定。

# Ansible Vaultの生パスワードファイルをGit管理から除外
.ansible_password

# キャッシュが出力されるディレクトリを除外
/.ansible_cache/

これくらいで始めて、必要に応じて設定を追加していく。

管理される側の準備

管理する側からのSSH接続を受け入れ、そのユーザーがsudoで操作ができるように設定する。

ansibleユーザーの作成

管理用に動けるユーザーを追加する。

$ sudo addgroup --system --gid 801 ansible-admin
info: Adding group `ansible-admin' (GID 801) ...

$ sudo adduser --system --uid 801 --gid 801 --shell /bin/bash --home /home/ansible-admin ansible-admin
info: Adding system user `ansible-admin' (UID 801) ...
info: Adding new user `ansible-admin' (UID 801) with group `ansible-admin' ...
info: Creating home directory `/home/ansible-admin' ...

ホームディレクトリを作る。

$ sudo mkdir -p /home/ansible-admin/.ssh

鍵を置く。

/home/ansible-admin/.ssh/authorized_keys

ssh-ed25519 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ansible-admin-key

権限を適正化する。

$ sudo chown -R ansible-admin:ansible-admin /home/ansible-admin
$ sudo chmod 700 /home/ansible-admin/.ssh
$ sudo chmod 600 /home/ansible-admin/.ssh/authorized_keys

パスワードなしでsudoできるようにする。

$ sudo visudo -f /etc/sudoers.d/ansible-admin
ansible-admin ALL=(ALL) NOPASSWD:ALL

ansibleユーザーの作成スクリプト

ansibleユーザーを各サーバーで作るのは億劫だし、ミスすると面倒くさい。
ということで、Geminiさんにスクリプトを作成してもらった。

bootstrap.sh

#!/bin/bash
set -e

# 公開鍵の文字列(先ほど確認した正しい公開鍵をここに記載します)
PUBKEY="ssh-ed25519 XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ansible-admin-key"

# 1. グループとシステムユーザーの作成
sudo addgroup --system --gid 801 ansible-admin
sudo adduser --system --uid 801 --gid 801 --home /home/ansible-admin --shell /bin/bash ansible-admin

# 2. .ssh ディレクトリと鍵の配置
sudo mkdir -p /home/ansible-admin/.ssh
echo "$PUBKEY" | sudo tee /home/ansible-admin/.ssh/authorized_keys > /dev/null

# 3. 権限と所有者の設定
sudo chmod 700 /home/ansible-admin/.ssh
sudo chmod 600 /home/ansible-admin/.ssh/authorized_keys
sudo chown -R ansible-admin:ansible-admin /home/ansible-admin/.ssh

# 4. sudoers.d 設定ファイルの作成
sudo install -m 640 -o root -g root /dev/null /etc/sudoers.d/ansible-admin
echo "ansible-admin ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/ansible-admin > /dev/null

echo "Setup completed successfully for ansible-admin!"

sudoers.dの配下にansible-adminを作る時、権限を640にしている。
Ubuntu 24.04でvisudoを使ったときにはこの権限でファイルが作られるのだけれど、以前は440だった。
権限を間違えると大変なので、ターゲットのシステムの状態を調べておくのが良いと思う。

接続テスト

管理するサーバーから接続をテストしてみる。

$ ssh -p 22 -i ~/.ssh/id_ed25519_ansible ansible-admin@hoge.hogeserver.hogeddns.jp

以下のテストでは、SSH接続し、Pythonインタプリターが正しく呼び出せるところまでをテストできる、とのこと。

$ ansible router -m ping
router | SUCCESS => {
"changed": false,
"ping": "pong"
}

これで、管理される準備ができた。

管理サンプル ~ ネットワークの管理

管理対象はUbuntu 24.04 serverで、Netplanで設定がされている。
また、ルーターはPPPoE接続でサービスを公開している。
これらの設定ファイルをAnsibleで管理することにした。

具体的には、設定ファイルを配布して、必要に応じて設定コマンドを実行する。
これらの行動をプレイブックという手順書に書き、それを起動する。
Ansible Community Documentation / Ansible playbooks
Ansible Community Documentation / Working with playbooks

ファイル構成

Ansibleはディレクトリの型にはめて管理すると、ディレクトリ指定が省略できて書きやすい。
このような構成で、roles配下にnetworkというディレクトリを作って書いていく。

ansible-sample/
├── ansible.cfg
├── .ansible_password
├── production.ini
├── deploy_network.yml
├── group_vars
│   └── all
│   └── vault.yml
└── roles
   └── network
   ├── tasks
   │   ├── main.yml
   │   ├── netplan.yml
   │   └── pppoe.yml
   ├── files
   │   ├── cloud-init.disabled
   │   ├── router
   │   │   └── 10-ansible-netplan.yaml
   │   ├── server1
   │   │   └── 10-ansible-netplan.yaml
   │   ├── server2
   │   │ └── 10-ansible-netplan.yaml
   │   └── routers
   │      ├── ip-up.d
   │      │   └── 9ddns
   │      ├── my-shutdown-script.service
   │      ├── my-shutdown-script.sh
   │      └── peers
   │      └── dsl-provider
   ├── templates
   │ └── routers
   │ └── chap-secrets.j2
   └── handlers
      └── main.yml

Netplan

設定ファイルを配布して、それを反映させる行動を書いていく。

roles/network/tasks/main.yml

今回は2種類の設定を管理することにしたので、ymlファイルを2つに分けた。
まずはnetplan.ymlを呼び出すようにする。

---
- name: Task deploy netplan
ansible.builtin.import_tasks: netplan.yml

roles/network/tasks/netplan.yml

大まかには、

  • Netplanの設定ファイルを配布
  • 配布した設定を反映
  • 設定ミスが発生したら設定を元に戻す(これは中途半端にしかできていない)

を実行する。

設定ミスが発生したときのために、ブロックを定義した。
Ansible Community Documentation / Blocks

---
- name: Apply Netplan configuration with automatic rollback on error
block:

Geminiさんオススメのcloud-initを止める設定。
Ubuntuのインストーラーが置いてくれるファイルだと思うので要らないよなーと思いつつ、あって良かったと思う日が来るのかもしれないので入れておいてみた。

/etc/cloud/cloud-init.disabledというファイルが存在していれば良いので、touchするだけでも問題はないが、せっかくなのでUbuntuが置いてくれたファイルを持ってきて、それを配布するようにした。

    - name: Deploy disable cloud-init message
ansible.builtin.copy:
dest: /etc/cloud/cloud-init.disabled
src: cloud-init.disabled
owner: root
group: root
mode: '0644'

Ubuntuをインストールしたときに自動で作られた設定ファイルを無効化する。
ファイルを削除しても良いとは思うが、ここでは名前を変えている。

    - name: Disable old netplan configuration files
ansible.builtin.command:
cmd: "mv {{ item }} {{ item }}.disabled"
removes: "{{ item }}" # ファイルがない場合にエラーを発生させない
loop:
- /etc/netplan/00-installer-config.yaml
- /etc/netplan/50-cloud-init.yaml

失敗したときに使うかもしれないので、1つ前に成功した設定ファイルを持ってきておく。
初回はファイルがないので、エラーを無視している。

    - name: Fetch netplan configuration files
ansible.builtin.fetch:
src: /etc/netplan/10-ansible-netplan.yaml
dest: "{{ fetch_dir }}/{{ inventory_hostname }}_10-ansible-netplan.yaml"
flat: true
ignore_errors: true

用意した設定ファイルをホストに配布する。
処理結果をdeploy_resultという変数に入れておく。

    - name: Deploy netplan configuration files
ansible.builtin.copy:
src: "{{ inventory_hostname }}/10-ansible-netplan.yaml"
dest: /etc/netplan/10-ansible-netplan.yaml
owner: root
group: root
mode: '0600'
register: deploy_result

配布したファイルが文法的に正しいか確認する。
チェック処理が成功したときに状態がchangedにならないように、changed when: falseとしている。

    - name: Validate netplan configuration syntax
ansible.builtin.command: netplan generate
when: deploy_result.changed
changed_when: false

設定を反映させる。
反映は非同期で行い、3秒間応答がなければ、タイムアウトと見なす。
また、実行結果を待たずに次のタスクに進む。

    - name: Confirm and permanently apply Netplan configuration
ansible.builtin.command: netplan apply # tryしても即座に確定するためテストは不可能
async: 3 # 通信ができなくなると処理が停止するため非同期で実行
poll: 0
when: deploy_result.changed

localhostから各ホストの指定ポート(デフォルトは22)がListenになっていることを確認する。
開始まで2秒待って、3秒後(都合5秒後)には、判定が終了する。

    - name: Verify SSH connectivity after configuration change
ansible.builtin.wait_for:
host: "{{ ansible_host }}"
port: "{{ ansible_port | default(22) }}"
state: started
delay: 2
timeout: 5
delegate_to: localhost
register: do_verify
when: deploy_result.changed

エラーが発生したときに実行する、例外処理ブロックを書く。

  rescue:

ファイルを配ったけれど、文法チェックで失敗したときに、ファイルを書き戻す。
ただし、設定が反映された後に接続できなくなった場合を除く(これはどうしようもない)。

    - name: "[RESCUE] Re-enable disabled old Netplan files"
ansible.builtin.copy:
dest: /etc/netplan/10-ansible-netplan.yaml
src: "{{ fetch_dir }}/{{ inventory_hostname }}_10-ansible-netplan.yaml"
mode: '0600'
when:
- do_verify is undefined
- deploy_result is defined
- deploy_result.changed

文法チェックに失敗したときには派手な出力がされるけれど、念押しで最後に状態を書き出しておく。

    - name: "[RESCUE] Fail playbook execution with error notification"
ansible.builtin.fail:
msg: >-
{{
'SSH connection lost after netplan apply. Remote rollback is unavailable.'
if do_verify is defined
else 'Netplan configuration failed before apply and was safely rolled back.'
}}

PPPoE(基本部分)

ルーターはPPPoE接続をしているので、それに関わるファイルを配布する。

production.ini

routersグループを追加し、routerを入れておく。

...
router ansible_host=192.168.110.10 ansible_port=22222

[routers]
router

roles/network/tasks/main.yml

routersグループを対象として、pppoe.ymlを起動する。

---
- name: Task deploy netplan
ansible.builtin.import_tasks: netplan.yml

- name: Task deploy pppoe
ansible.builtin.include_tasks: pppoe.yml
when: "'routers' in group_names"

roles/network/tasks/pppoe.yml

ここから、実際にファイルを配布して設定するタスクを書く。

まずは接続設定のファイルの配布だが、ユーザーIDとパスワードが書き込まれている。
そのため、このファイルに直接パスワードを書かずに、変数が展開されるようにしテンプレートを使う。
テンプレートの詳細は後述。

---
- name: Deploy PPPoE secrets files
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dst }}"
owner: root
group: root
mode: '0600'
loop:
- { dst: "/etc/ppp/chap-secrets", src: "routers/chap-secrets.j2" }

内容が変わらないファイルは単にコピーしている。
接続開始したらDynamicDNSにIPアドレスを通知するスクリプトと、接続時の設定ファイル。

- name: Deploy PPPoE configuration files
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dst }}"
owner: root
group: "{{ item.grp }}"
mode: "{{ item.mode }}"
loop:
- { dst: "/etc/ppp/ip-up.d/9ddns", grp: "root", mode: "0755", src: "routers/ip-up.d/9ddns" }
- { dst: "/etc/ppp/peers/dsl-provider", grp: "dip", mode: "0640", src: "routers/peers/dsl-provider" }

ここで、シャットダウン時に処理が実行されるように仕掛ける。
ただ、ファイルを置いただけでは機能しないので、通知を出しておく。

- name: Deploy unit files for shutdown service
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dst }}"
owner: root
group: root
mode: "{{ item.mode }}"
loop:
- { dst: "/etc/systemd/system/my-shutdown-script.service", mode: "0644", src: "routers/my-shutdown-script.service" }
- { dst: "/usr/local/sbin/my-shutdown-script.sh", mode: "0755", src: "routers/my-shutdown-script.sh" }
notify: Reload and enable my-shutdown-script

roles/network/handlers/main.yml

通知を受け取るハンドラーは、handlersに作成しておくと、ロールが読み込まれたときにハンドラーとして登録される。
ハンドラーはnotifyされた名前のものが反応する。
※他の名前で作ったタスクも実行したいときは、listen: "<notifyした文字列>"と書いておけばOK。

- name: Reload and enable my-shutdown-script
ansible.builtin.systemd_service:
name: my-shutdown-script.service
daemon_reload: true
enabled: true

PPPoE(ユーザー・パスワードの暗号化)

テンプレートには、「ここに指定の値を入れてください」とだけ書いておく。
ユーザー・パスワードを暗号化したファイルに書いておき、その箇所に値を入れる。

ユーザー・パスワードは暗号化されているので、Gitに保管して公開してもそのままは読めない、といった仕組み。

roles/network/templates/routers/chap-secrets.j2

ユーザー・パスワードが書き出されるテンプレートはこういったもの。

# Secrets for authentication using CHAP
# client server secret IP addresses

"{{ vault_pppoe_user }}" * "{{ vault_pppoe_pass }}"

group_vars/all/vault.yml

この値を設定するのに、以下のコマンドでファイルを作成する。

$ ansible-vault create group_vars/all/vault.yml
vault_pppoe_user: pppuser@example.com
vault_pppoe_pass: "ppp-password"

ansible-vaultで作成したファイルは、.ansible_passwordに書き込んだパスワードを基に暗号化される。
また、ランダムなSaltが生成されて利用されているとのこと。

$ cat group_vars/all/vault.yml
$ANSIBLE_VAULT;1.1;AES256
61633534373131663634343661386666376562396438613737653432306633626439323566383861
3135356563333165316636376432353665633766636531660a633036313061616130306338636562
34616431663538326665366136373230363932396166323262616261356433633366313033336530
6465666665623333350a346135323362393964623161303533396562373261646565303963623233
33376230396339363763626536303039363363363066373034313634613261626566646239323165
30336130643338366138663030373064343631616561636363313965373932323532343631616432
63646530386666656130393135313261306632616134633831636566363832323832343036336330
64396434303866663363

このファイルを編集するときには、以下のコマンドが利用できる。

$ ansible-vault edit group_vars/all/vault.yml

タスクの実行

deploy_network.yml

ロールnetworkのタスクを起動するdeploy_network.ymlを作成。
これで、roles/network/tasks/main.ymlが実行される。

---
- name: Deploy network configuration files
hosts:
- all_servers
become: true
roles:
- network

プレイブックの再生

ansible-sampleディレクトリでドライランしてみる。

$ ansible-playbook deploy_network.yml --diff --check

ホストを限定することもできる。

$ ansible-playbook deploy_network.yml --diff --check --limit router

問題なさそうであれば再生。

$ ansible-playbook deploy_network.yml --diff

テクニック

生成AIに聞けばいくらでも書き方を教えてくれるが、その聞く手間も省けるのでメモ。
質問すると電力をかなり消費する、ってことなのでメモはエコ。

特定のグループに含まれている時だけ実行

前述の説明と重複になるけれど、all_serversで処理をしているけれど、routersに含まれる場合にだけタスクを実行することができる。

- name: Task deploy pppoe
ansible.builtin.include_tasks: pppoe.yml
when: "'routers' in group_names"

1つのタスクでいくつかのファイルをコピーする(直接指定)

loopでパラメーターをそれぞれ指定してファイルがコピーできた。
この先のcopyもそうだけれど、templateに変えて使うこともできた。

- name: Deploy router worker scripts
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dst }}"
owner: "{{ item.own }}"
group: "{{ item.grp }}"
mode: "{{ item.mode }}"
loop:
- { dst: "/usr/local/sbin/update-dns.sh",
own: "root", grp: "root", mode: "0755", src: "router/update-dns.sh" }

1つのタスクでいくつかのファイルをコピーする(変数指定)

host_varsやgroup_varsのファイルで変数としてファイルを指定する。
ホスト単位、グループ単位でのファイル配布が可能になる。

host_vars/router.yml

cron_worker:
- { dst: "/usr/local/sbin/update-dns.sh",
own: "root", grp: "root", mode: "0755", src: "routers/update-dns.sh" }

タスクで、このcron_workerでループさせる。

- name: Deploy cron worker files
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dst }}"
owner: "{{ item.own }}"
group: "{{ item.grp }}"
mode: "{{ item.mode }}"
loop: "{{ cron_worker | default([]) }}"

host_varsやgroup_varsで定義されていない場合は、スキップした。

ディレクトリにあるファイルを1つ1つコピーする

srcにディレクトリを指定すると、複数ファイルのコピーが1つのタスクとしてコピーされる。
--diffでは、変更があったファイルが何かが表示される。

この書き方をすると、ファイル1つ1つがコピーされる。
--diffでは、ファイルの1つ1つの差分が表示される。

- name: Deploy conf configuration
ansible.builtin.copy:
src: "{{ item }}"
dest: "/etc/apache2/conf-available/"
owner: root
group: root
mode: '0644'
loop: "{{ query('ansible.builtin.fileglob', 'conf-available/*') }}"
notify: Reload apache2

- name: Deploy sites configuration
ansible.builtin.copy:
src: "{{ item }}"
dest: "/etc/apache2/sites-available/"
owner: root
group: root
mode: '0644'
loop: "{{ query('ansible.builtin.fileglob', 'sites-available/' ~ inventory_hostname ~ '/*') }}"
notify: Reload apache2

ちなみに、~は文字列連結処理だそう。

変数の中身をファイルとしてコピー

管理しているサーバーから、さらに別のサーバーに接続して情報を取得する、という処理を書いている。
接続には秘密鍵を使用しており、その秘密鍵を配布する方法を聞いてみた。

group_vars/all/vault.yml

vault_ssh_private_key: |
-----BEGIN OPENSSH PRIVATE KEY-----
<鍵の内容>
-----END OPENSSH PRIVATE KEY-----

タスクでは、srcの代わりにcontentとして変数を指定すると、内容がファイルに出力された。

- name: Deploy goodluck secret key
ansible.builtin.copy:
content: "{{ vault_ssh_private_key }}"
dest: /etc/ssl/private/other.key
owner: root
group: root
mode: "0600"

リスト管理されたファイルをコピーして不要ファイルを削除

Fail2banで.localファイルを展開するとき、配布済みだが使わなくなったファイルは削除したい。
.confファイルが大量に置いてあるので、同期というわけにもいかない。

この場合、defaultsというディレクトリにリストを作っておくことができるらしい。

roles/fail2ban/defaults/main.yml

# アクションファイルの一覧
jail_action_map:
- nftables-common.local

リストを読み込む処理をGeminiさんに作ってもらった。
リストにあるファイルを展開した後、リストに含まれないファイルを削除している。

roles/fail2ban/tasks/main.yml

# 有効なJailに必要なフィルタのみを集約したリスト(active_actions)を生成
- name: Set active action list
ansible.builtin.set_fact:
active_actions: >-
{{
jail_action_map
| flatten
}}

# 有効なフィルタのみをサーバーに配置
- name: Deploy active action.d files
ansible.builtin.template:
src: "action.d/{{ item }}"
dest: "/etc/fail2ban/action.d/{{ item }}"
owner: root
group: root
mode: '0644'
loop: "{{ active_actions }}"
notify: Reload fail2ban

# ターゲットサーバー上の既存 .local ファイル一覧を取得
- name: Find existing custom action files on target
ansible.builtin.find:
paths: /etc/fail2ban/action.d
patterns: "*.local"
register: found_action_files

# active_actions に含まれていない .local ファイルを削除
- name: Remove inactive action.d files
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ found_action_files.files }}"
when: (item.path | basename) not in active_actions
loop_control:
label: "{{ item.path }}"
notify: Reload fail2ban

リスト管理されたファイルをコピーして不要ファイルを削除(カテゴリ指定)

フィルターに置くファイルは、提供しているサービス毎に違ったりしている。
Apacheを運用していないサーバーにApacheのファイルを配っても仕方がない。
(あって困るわけではないが、あって困るケースを想定した学習として)

defaultsディレクトリにリストを作っておく。

roles/fail2ban/defaults/main.yml

# サービスとフィルタファイルの一覧
jail_filter_map:
recidive:
- recidive.local
apache:
- apache-auth.local
- apache-404.local
- apache-410.local
- apache-modsecurity.local
postfix:
- postfix.local
sshd:
- sshd.local

ホストで使用するカテゴリーを指定しておく。

host_vars/server1.yml

# Fail2ban
enabled_jails:
- recidive
- sshd
- apache
- postfix

リストを作る処理はGeminiさんに作成してもらったが、Geminiさんでさえ数回のトライが必要な複雑さ。
リストができた先は、前節と同じ処理。

roles/fail2ban/tasks/main.yml

# 有効なJailに必要なフィルタのみを集約したリスト(active_filters)を生成
- name: Set active filter list
ansible.builtin.set_fact:
active_filters: >-
{{
enabled_jails
| default([])
| map('extract', jail_filter_map)
| reject('undefined')
| flatten
}}

# 有効なフィルタのみをサーバーに配置
- name: Deploy active filter.d files
ansible.builtin.template:
src: "filter.d/{{ item }}"
dest: "/etc/fail2ban/filter.d/{{ item }}"
owner: root
group: root
mode: '0644'
loop: "{{ active_filters }}"
notify: Reload fail2ban

# ターゲットサーバー上の既存 .local ファイル一覧を取得
- name: Find existing custom filter files on target
ansible.builtin.find:
paths: /etc/fail2ban/filter.d
patterns: "*.local"
register: found_filter_files

# active_filters に含まれていない .local ファイルを削除
- name: Remove inactive filter.d files
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ found_filter_files.files }}"
when: (item.path | basename) not in active_filters
loop_control:
label: "{{ item.path }}"
notify: Reload fail2ban

ディレクトリにあるファイルを同期

vimの日本語マニュアルを全サーバーに展開しようと思い、ディレクトリを指定してcopyしたが、1つ1つハッシュ化して内容比較して…となるので、変化がなくても比較にとても時間が掛かる。

そこで、vimのカスタマイズは専用のディレクトリを作るので、ディレクトリを同期することにした。
これは早い。

マニュアルを以下からクローンさせてもらって、9.1の最終と思われるコミットをチェックアウトしている。
Github / vim-jp / vimdoc-ja
このディレクトリを同期させる。

- name: Deploy manual-ja files
ansible.posix.synchronize:
src: vimdoc-ja/
dest: /usr/share/vim/vimfiles/vimdoc-ja/
delete: yes
rsync_opts:
- "--chown=root:root"
- "--chmod=D755,F644"
- "--exclude=.git*"
- "--exclude=*.md"

最初にこれを実行した際、以下のメッセージが表示された。

TASK [vim : Deploy manual-ja files] ****************************************************************************************************************************

[DEPRECATION WARNING]: The connection's stdin object is deprecated. Call display.prompt_until(msg) instead. This feature will be removed in version 2.19.

Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.

動いてはいるので、メッセージを無効化しても問題はない。
ただ、Geminiさんがいくつか選択肢を用意してくれていて、その中にansible.posixを最新化する、というのがあったので実行してみた。

$ ansible-galaxy collection install ansible.posix --upgrade

これによってコレクションが1.5.4から2.2.2に更新され、この警告は表示されなくなった。

サービス関連の操作

サービスをカスタマイズするとき、/etc/systemd/system/<service name>.service.d>/override.conf にファイルを置いて、daemon-reloadすることができる。

- name: Ensure systemd override directory exists for nftables.service
ansible.builtin.file:
path: /etc/systemd/system/nftables.service.d
state: directory
owner: root
group: root
mode: '0755'

- name: Deploy override file for nftables.service
ansible.builtin.copy:
src: nftables.service.d/override.conf
dest: /etc/systemd/system/nftables.service.d/override.conf
owner: root
group: root
mode: '0644'
register: nft_override

- name: Reload systemd daemon if override changed
ansible.builtin.systemd_service:
daemon_reload: true
when: nft_override.changed

サービスの設定を変えてreloadすることもできる。
reloadedのところを、restartedにすると、サービスがrestartする。

- name: Deploy nftables.conf to production router
ansible.builtin.template:
src: "{{ nftables_template_src }}"
dest: /etc/nftables.conf
owner: root
group: root
mode: '0750'
trim_blocks: true
lstrip_blocks: true
register: nft_config

- name: Run syntax check on the remote router
ansible.builtin.command: nft -c -f /etc/nftables.conf
changed_when: false
when: nft_config.changed

- name: Reload nftables service if configuration changed
ansible.builtin.systemd_service:
name: nftables
state: reloaded
when: nft_config.changed

高速化

gathering

ファクトの収集には時間が掛かる…ということなのだが、そもそもいつ何を集めているのかをGeminiさんに聞いてみた。

Ansibleにおける「ファクト(Facts)」とは、ansible.builtin.setup モジュールによってリモートホストから自動的に収集されるシステム上の詳細な情報です。

  • 収集される情報の具体例:
    • OSのディストリビューション、バージョン、カーネル情報
    • IPアドレス、MACアドレス、ネットワークインターフェースの詳細
    • CPUのコア数、アーキテクチャ
    • メモリ容量、ディスクのパーティション・マウント状況
    • ホスト名、環境変数 など

デフォルトの状態では、プレイブックの実行開始時(タスクが実行される前)に、すべてのターゲットホストに対してこの情報収集プロセス(setupモジュールの実行)が自動的に挟まります。ホスト数が多くなると、このファクト収集だけで大きな時間的オーバーヘッドになります。

収集方法には3種類の設定があるということだった。
Ansible Community Documentation / Ansible Configuration Settings - DEFAULT_GATHERING
Ansible Community Documentation / ansible.builtin.jsonfile cache – JSON formatted files.

ansible.cfg

[defaults]
# 設定1: ファクトの自動収集をオンにする
gathering = implicit

# 設定2: ファクトを自動収集してキャッシュする
# fact_caching_timeout = 0 は無期限、デフォルトは86400(1日)
gathering = smart
fact_caching = jsonfile
fact_caching_prefix =
fact_caching_timeout = 0
fact_caching_connection = .ansible_cache

# 設定3: ファクトの自動収集をオフにする
gathering = explicit

本番環境(管理対象サーバー7台)で時間を計ってみた。

$ time ansible-playbook deploy_cron.yml --diff
...
real 0m29.034s
user 0m3.881s
sys 0m3.160s

設定値1回目2回目3回目平均
implicit30.0s28.5s29.0s29.1s
smart29.8s25.4s25.4s26.9s
explicit25.8s25.6s25.4s25.6s

操作されるサーバー側もキャッシュが利いたりするのかもしれないので時間は安定しないが、ファクト取得を省略すると、ザックリ4秒くらい短縮できることが分かった。
保存されたキャッシュをみると、大きいもので100KB程度あり、平均で50KBほどあった。

smartにした場合、fact_caching_connection に指定したディレクトリが自動で作成された。
環境が変わったら、環境が変わったサーバーのファイルを削除するか、ディレクトリを削除する。

タスク毎に個別にオフの設定もできるということだった。

- name: Configure logcheck filter
hosts:
- all_servers
gather_facts: false
become: true

pipelining

これはGeminiさんが教えてくれた設定で、SSHでの処理を最適化しようとするもののようだ。

ansible.cfg

[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

マニュアルには、default、connection、ssh_connectionのセクションに設定が可能と書かれている。
他のモジュールに影響を与えないように、ssh_connectionに設定すること推奨、だそうだ。
Ansible Community Documentation / ansible.builtin.ssh connection – connect via SSH client binary

Geminiさんに聞いてみたところ、すぐに答えを教えてくれた。

Ansibleはデフォルトの状態(pipelining = False)では、リモートホスト上でタスク(モジュール)を実行する際、以下のような手順を踏みます。

  1. SSH接続の確立: 対象サーバーへのSSHセッションを開く。
  2. 一時ディレクトリの作成: リモートサーバー上のテンポラリ領域(例: ~/.ansible/tmp/ など)に一時ディレクトリを作成する。
  3. モジュールファイルの転送: Ansibleのモジュール(実体はPythonスクリプト等)を、SFTPやSCPなどのファイル転送手段を使ってリモートサーバーのその一時ディレクトリへファイルとして書き込む
  4. モジュールの実行: 別途SSHコマンドを発行し、書き込んだファイルを実行する。
  5. 結果の回収と後片付け: 実行結果(JSONなど)を受け取った後、リモートサーバー上に作成した一時ファイルを削除し、ディレクトリを片付ける

pipelining = True の場合には、ディスクへの書き込み・削除といったファイル操作や、追加のSFTP/SCP転送にかかる通信の往復(オーバーヘッド)がごっそり削られ、処理が高速化します。

  • pipelining = True を有効にすると、Ansibleはモジュールファイルをリモート側のディスクへファイルとして保存(転送・削除)するプロセスをバイパスします。
  • その代わり、SSHの標準入力(stdin)のパイプラインを経由して、モジュールのコードをリモートサーバー側で待機しているPythonインタプリタへ直接流し込みます

だったらデフォルトにすれば良いのに、と思ったが、そうも行かない事情も教えてくれた。

pipelining = True は非常に効果的なチューニングですが、利用する環境において以下の注意点を確認する必要があります。

  • sudorequiretty 設定との競合: リモートホスト側で sudo を使用する際、requiretty(TTYが割り当てられていないとsudoを実行できない設定)が有効になっている場合、stdin経由でコードを流し込むpipeliningはエラーを引き起こすことがあります。近年のLinuxディストリビューションではデフォルトで無効化されていることがほとんどですが、古い環境やセキュリティが厳格な環境では確認が必要です。

より多くの環境で動作する設定をデフォルトにして、追加設定で高速かができるようにしている、ということのようだが、うちの環境では問題なく動作した。

ssh_args(設定不要)

これはGeminiさんが教えてくれた設定で、pipeliningの続き。

ansible.cfg

[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

この設定値はデフォルトであることが分かったので、Geminiさんに聞いてみた。

  • 初期のAnsibleの仕様: Ansibleの初期のバージョンや、SSHの古い実装、あるいは一部のエンタープライズ系Linuxディストリビューション(古いRHEL 6など)では、デフォルトで ControlPersist がサポートされていなかったり、有効になっていなかったりした。そのため、昔の環境ではタスクごとに毎回SSHコネクションが張り直され、非常に動作が重かった。
  • 標準デフォルトへの組み込み: その後のOpenSSHの進化と普及に伴い、Ansibleはパフォーマンスを飛躍的に向上させるため、現在ではこの ControlMaster=auto -o ControlPersist=60s(および圧縮を意味する -C)を標準のデフォルト値として組み込むようになった。

実際に、無指定で-vvvを付けて実行すると、このオプションが付いていることが確認できた。
よって設定せずとも問題なし。

タスクの並列実行

上でテストしたcronの設定展開は、30秒かかる効率が悪いものだった。
きめ細かなタスク実行が可能な書き方ではあるが、タスクはそれぞれ単発で実行される。

roles/cron/tasks/main.yml

- name: Task all servers
ansible.builtin.include_tasks: "{{ inventory_hostname }}.yml"
ignore_errors: false

roles/cron/tasks/server1.yml

---
- name: Deploy server1 worker scrpits
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dst }}"
owner: "{{ item.own }}"
group: "{{ item.grp }}"
mode: "{{ item.mode }}"
loop:
- { dst: "/usr/local/sbin/backup_daily",
own: "root", grp: "root", mode: "0755", src: "server1/backup_daily" }

- name: Deploy server1 secret files
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dst }}"
owner: "{{ item.own }}"
group: "{{ item.grp }}"
mode: "{{ item.mode }}"
loop:
- { dst: "/usr/local/etc/backup_secret",
own: "root", grp: "root", mode: "0600", src: "secret/backup_secret.j2" }

これでは並列で処理できないので、タスクを1つにして、コピーするファイルを編集で指定した。
具体的には「1つのタスクでいくつかのファイルをコピーする(変数指定)」のやり方に変更した。

結果として、処理は15秒で完了するようになった。

Ansibleのコマンド群

Geminiさんにコマンドの一覧を作って、と依頼した結果。
この記事を投稿する時点までに使ったコマンドに星印を付けてみる。

コマンド主な役割・用途実務での使用頻度・シーン
ansible単発のコマンドをリモートで実行する(Ad-hocモード):接続テストや、全サーバーの一斉再起動、簡易的な状態確認など。
ansible-playbook定義書(Playbook)に沿って一連の構築・設定を自動実行する最高:Ansibleのメイン機能。環境構築やアップデートの自動化はすべてこれで行います。
ansible-vaultパスワードや秘密鍵などの機密ファイルを暗号化・復号する:Gitなどの共有リポジトリに、パスワードや機密設定を安全にコミットしたい時に必須です。
ansible-galaxy世界中の人が公開している共通パーツ(RoleやCollection)を導入・管理する:外部の便利な構築スクリプトを利用したり、自作のパーツをテンプレート化する際に使います。
ansible-doc各種モジュール(設定用の関数)の公式マニュアルを端末上に表示する:「あのモジュールの書き方(パラメータ)なんだっけ?」と調べたい時に、ブラウザを開かず確認できます。
ansible-inventory管理対象サーバーの一覧(インベントリ)の構成や変数を検証・出力する:設定したサーバーリストが正しくAnsibleに認識されているか、JSON形式などでデバッグする際に使用します。
ansible-configAnsible自体の設定(ansible.cfg)の確認や表示、検証を行う:現在の設定値の一覧を見たり、デフォルト値から変更されている部分を探す時に使います。
ansible-consoleインタラクティブ(対話型)な画面で、プロンプトと対話しながらAnsibleを動かす:シェル感覚で1行ずつリモートサーバーを操作したい場合に使いますが、実務での出番は少なめです。
ansible-pullサーバー側から設定を取りに行く「プル型」の実行スタイルを提供する:通常は管理側から突っ込む「プッシュ型」ですが、起動時に自発的に設定を反映させたい特殊な環境で使います。
ansible-testAnsibleのモジュールやプラグイン開発者が、コードのテストを行う極低:Ansibleの「ツール自体」を開発・拡張する人向けのコマンドです。通常のインフラ運用では使いません。
ansible-connectionバックグラウンドで接続(プラグイン)の維持などを管理する内部用コマンドなし:Ansibleが内部システムとして自動で呼び出すものなので、人間が直接叩くことはありません。
ansible-communityコミュニティ版の各種パッケージ情報や関連ユーティリティへのポインタなし:パッケージ構成に応じた情報確認用のコマンドで、直接運用で叩くことはありません。

タスクの書き方一つとってもそうだけど、まだまだ高度に使う余地があると思ったのだった。

さいごに

今回も、Geminiさんがいないととても実装はできないレベルだった。

型にはめたディレクトリ構造にすること、パスワードをハッシュ化して保管すること、要らなくなったファイルを削除すること、等々、キリがないほど質問しては試しての繰り返しだった。

タスクの書き方一つとっても全然分からなかったし、一度実装したものも、別のところで実装しようとするときに忘れていたりする。
Geminiさんは、根気強く何度でも教えてくれる。

今回のテーマについていうと、マニュアルが充実していたり、先輩エンジニア達が提供してくれる情報が正確だったりするのだろう、ほとんど間違いもなく正確に回答してくれたこともありがたかった。

ただし、これでも集中管理のスタートラインに立っただけ、という感覚だったりする。
7月中旬から始めてもう2ヶ月、コミット数は230を超えていて、この期間でセキュリティを強化したりもした。
とはいえ、各サーバーからファイルを持って来て、ディレクトリだけを揃えただけのものが結構ある。

ようやく最適化や、中身の整理に入っていける状態になったけれど、最終的にはディザスターリカバリーですって!?
ゆっくりやっていこう。

ここから広告
広告
広告ここまで

コメントはこちらから お気軽にどうぞ ~ 投稿に関するご意見・感想・他