💻

Ansibleを勉強がてら試してみる

2023/12/09に公開

会社でデータ分析部隊が使う GPU サーバー複数台をオンプレ環境で維持保守しているのだけれど、本業のアプリ開発・システム構築の空き時間でサーバーメンテをしていることもあって極力時間を使わずに済むようにしたい。ということで以前から気になっていた Ansible の勉強をしてみることにした。勉強のゴールは以下 2 点。

  1. 複数サーバーの定期メンテナンスをコマンド一発で行えるようにしたい
    • 主に日々の OS 更新ですね
  2. 各サーバーの構成を Git で管理したい
    • もう少し細かく言うと「サーバーの構成情報がテキストファイルで定義され、それが実態と同期され、さらにバージョン管理される」ようにしたい

前提条件

以下の 3 ノードしか登場しない、超シンプルな構成を前提とする。

Hostname OS IP Address
rocky Rocky Linux 8 192.168.56.30
worker1 Ubuntu 22.04 192.168.56.31
worker2 Ubuntu 22.04 192.168.56.32

このうちrockyをコントロールノードとし、worker1worker2、およびrocky自身をターゲットノードとして制御する。コントロールノード自身も Ansible で構成管理したいという考え。

構築手順

概要

次のような手順で試していく。

  1. VirtualBox で仮想マシンを作成
    1. OS インストール
    2. ユーザー"ansible"の作成
  2. Ansible と Python をインストール
  3. Ansible を使う
    1. 疎通確認をする
    2. アドホックなコマンドの一斉投入を行う
    3. Playbook で一斉 OS 更新を行う
  4. おまけ: Playbook で Docker をインストールする

上記作業により、冒頭に記したゴールは達成できた。ただし 「構成ファイルを更新してもサーバーへ自動的に反映されるようにはなっていない」 という課題が残っている。そのような自動化の仕組みは追加で作る必要がある…コントロールノードで git pull して playbook を再生するような cron ジョブを仕込むだけで実現はできそうだけれど、それが最善なのかは今後勉強していきたい。

なお当然だけれど各サーバーで勝手(?)に構成を変えるような操作をしてしまった場合、その変更を Ansible が知るはずもなく構成ファイルと実態の間で齟齬が発生してしまう。そういう操作は原則禁止という運用ルールを作る必要があるのかなと思う。

1. VirtualBox で仮想マシンを作成

1.1 OS インストール

お試しなので、標準設定のまま各仮想マシンを作成して OS をインストールする。
ただしネットワークアダプタは以下のように構成した:

  • 1 つ目
    • NAT に接続
    • IPv4 で DHCP を使用, IPv6 は無効化
  • 2 つ目のネットワークアダプタ
    • Host-only Adaptor に接続
    • IPv4 で以下のように固定 IP アドレスを設定, IPv6 は無効化
      • コントロールノード: 192.168.56.30/24
      • ターゲットノード 1: 192.168.56.31/24
      • ターゲットノード 2: 192.168.56.32/24

この構成を採用したのは以下記事を参考にさせてもらってのこと。

https://qiita.com/aki3061/items/ea35094c5dc4699d2a6b

1.2 ユーザー"ansible"を作成

Ansible による自動制御で使う、ユーザーアカウント"ansible"を全サーバーにて作成する。

まずコントロールノードでユーザーを作成し、パスワード入力なしで sudo を実行できるようにする。なお、今回は ansible でコントロールノード自体の制御も行えるようにするため、コントロールノードでも設定を行っている:

sudo useradd -m ansible
sudo passwd ansible
echo 'ansible ALL=(ALL) NOPASSWD: ALL' | sudo EDITOR='tee -a' visudo

ここは以下のページを参考にさせていただきました:

https://qiita.com/eurogrve/items/16e17dca221d48974fab

続いて、コントロールノードでパスワードレス認証可能な SSH 鍵ペアを作成する:

$ su -l ansible  # ansibleユーザーに代わる
Password: [ユーザーansibleのパスワードを入力]
[ansible@rocky ~]$ ssh-keygen -t ed25519 -P '' -f ~/.ssh/id_ed25519
Generating public/private ed25519 key pair.
..()..

さらに、コントロールノード自身に ansible ユーザーで SSH ログインできるように鍵を送り込む:

[ansible@rocky ~]$ ssh-copy-id -o StrictHostKeyChecking=no -i ~/.ssh/id_ed25519.pub localhost
...()...
ansible@localhost's password: [パスワードを入力]
...()...
[ansible@rocky ~]$ ssh localhost -- whoami  # 自分自身へのSSHログインを試す
ansible  # ← 成功すると、このようにログインユーザー名のみが表示される

ここでは以下ページを参考にさせてもらった。

https://qiita.com/umeneri/items/a59680a27f500e34edee

これと同様の作業をターゲットノードに対しても実行していく:

[ansible@rocky ~]$ ssh-copy-id -o StrictHostKeyChecking=no -i ~/.ssh/id_ed25519.pub 192.168.56.31
...()...
ansible@192.168.56.31's password: [パスワードを入力]
...(略)...

[ansible@rocky ~]$ ssh-copy-id -o StrictHostKeyChecking=no -i ~/.ssh/id_ed25519.pub 192.168.56.32
...(略)...
ansible@192.168.56.32's password: [パスワードを入力]
...()...

最後に、コントロールノードの ansible ユーザーから、コントロールノードとターゲットノード 2 台すべてに対してパスワード入力を要求されずに SSH 越しで特権コマンドを投入できることを確認しておく:

[ansible@rocky ~]$ for x in 30 31 32; do
> ssh 192.168.56.$x -- sudo whoami  # パスワードを聞かれず3回rootと表示されればOK
> done
root
root
root

2. Ansible と Python をインストール

コントロールノードで Ansible をインストールする。Rocky Linux 8 の場合、OS 標準のパッケージレポジトリに Ansible 2.15 が含まれていたので素直にそれをインストールする:

sudo dnf install -y ansible-core

続いて、ターゲットノードに Python インストールする。動作要件に合うバージョンの Python を使用する必要があるが、Rocky Linux 8 の方ではPython3.11パッケージを、Ubuntu 22.04 の方ではpython3パッケージを使用した。なお前者の場合はansible-coreをインストールした時点で依存関係として勝手にインストールされており、また後者の場合は OS セットアップの時点で最初からインストールされていた。したがって特別にインストール操作は不要だったけれど、もしインストールするなら、それぞれ以下のようなコマンドでインストールできる:

  • Rocky Linux 8: sudo dnf install python3.11
  • Ubuntu 22.04: sudo apt install python3

3. Ansible を使う

以下、特に断りがない限りコマンド入力は以下の条件で行うものとする:

  • コントロールノード rocky にユーザー ansible でログインしている
  • ディレクトリを /home/ansible/ansible-test が作成済みである
  • カレントディレクトリを /home/ansible/ansible-test に設定している

3.1 疎通確認をする

まずは Ansible の疎通確認、つまりコントロールノードからターゲットノードに正しく接続できるか確認する。

最初に、インベントリ定義ファイルを作成します。インベントリとは操作対象のグループに名前を付けたもの、と雑に理解している。今回はinventory.yamlというファイルに以下の内容を書き込んだ。

inventory.yaml
ungrouped:
  hosts:
    192.168.56.30:
workers:
  hosts:
    192.168.56.31:
    192.168.56.32:

この定義ファイルを使うと、ホスト名パターンworkersを指定すると Ubuntu の 2 台が対象となり、パターンallを指定すると Rocky Linux を含む 3 台すべてが処理対象となる。

では、疎通確認。Ansible 標準で用意されているモジュール"ping"を使って全台との疎通確認を実施:

[ansible@rocky ansible-test]$ ansible -i inventory.yaml all -m ansible.builtin.ping -o
192.168.56.31 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
192.168.56.32 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
192.168.56.30 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}

SUCCESS ということで、成功した。なお記事の面積の都合で-oを付けたけれど指定しなくともよい。

3.2 アドホックなコマンドの一斉投入を行う

お作法的には良くないと思いつつ便利な場面も多いし、ということでshell モジュールを使ったアドホックなコマンドの一斉投入を試してみる。

このモジュールは疎通確認で使ったpingと違って実行するコマンドラインをパラメーターとして指定する必要があるため-aオプションを使う。またリモートで sudo を使った特権昇格ができることも確認したいので-bオプションを付けて当該コマンドラインを sudo で実行するようにしてみる:

[ansible@rocky ansible-test]$ ansible -i inventory.yaml all -m ansible.builtin.shell -o -b -a 'whoami'
192.168.56.32 | CHANGED | rc=0 | (stdout) root
192.168.56.31 | CHANGED | rc=0 | (stdout) root
192.168.56.30 | CHANGED | rc=0 | (stdout) root

3 台すべてから"root"とだけ返ってくることが期待されるので、上記のように表示されれば成功。

3.3 Playbook で一斉 OS 更新を行う

やっと Ansible らしくなってきました。Playbook による制御を試します。具体的なタスクは、OS のパッケージ更新を全ノードに対して一斉実行させる内容。

まずコントロールノードに Playbook ファイルを作る。今回はos-update.yamlというファイルに以下の内容を書き込んだ:

os-update.yaml
- hosts: all
  gather_facts: true
  tasks:
    - name: Check OS family
      ansible.builtin.debug:
        var: ansible_os_family

    - name: Upgrade all packages (apt)
      become: yes
      ansible.builtin.apt:
        name: "*"
        state: latest
      when:
        - ansible_os_family == "Debian"

    - name: Upgrade all packages (dnf)
      become: yes
      ansible.builtin.dnf:
        name: "*"
        state: latest
      when:
        - ansible_os_family == "RedHat"

この内容は、すべてのノード (all) に対して、OS ファミリーに応じて apt または dnf を使った OS パッケージ更新を行う。ただし、各ノードの OS ファミリーが何であるかを確認できるよう ansible.builtin.debug モジュールを使って ansible_os_family 変数の値をコンソールにデバッグ表示する内容も含めている。

これを、以下のコマンドで再生する:

[ansible@rocky ansible-test]$ ansible-playbook -i inventory.yaml -b os-update.yaml
...()...
PLAY RECAP ****************************************************************************************************************************
192.168.56.30              : ok=3    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
192.168.56.31              : ok=3    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
192.168.56.32              : ok=3    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

どのノードでも更新が無く変更が発生しなかった場合、上記のような出力になる。

4 おまけ: Playbook で Docker をインストールする

Docker を worker1worker2 を対象にインストールする。

2023 年 12 月 8 日現在の Docker 公式インストール手順では Docker 公式の GPG 鍵のインポートと apt レポジトリの追加登録が必要。これらも Ansible で実行可能だと思うけれど、今回はコマンドを Ansible のタスクに「翻訳」する時間が無いので個々のサーバーでインストール作業をした。つまり、apt install docker でインストール可能な状態にまでセットアップしておいた。

(Docker に限らず公式のインストール手順では実行すべきコマンドなどが並べられることが多いけれど、Ansible 使いの方々としては全部「翻訳」してプレイブックを作り上げるのだろうか。まあ台数が多ければ労力に見合う価値があると思うけれど、5 ~ 6 台といった程度だと悩ましい。。)

で、Docker をインストールするための Playbook は以下のようになる:

install-docker.yaml
- hosts: workers
  tasks:
    - name: Install Docker (apt)
      become: yes
      ansible.builtin.apt:
        name:
          - docker-ce
          - docker-ce-cli
          - containerd.io
          - docker-buildx-plugin
          - docker-compose-plugin
        state: latest

インストール作業の手間だけで考えると、普通に各サーバーで apt install した方が良いように感じてしまったので Ansible でやるメリットを考えてみる。この例では構成がシンプルすぎて実感が沸かないけれど、おそらくサーバー構成情報(の一部であるインストールパッケージ名のリスト)がファイルで定義されていることに意味があるのだと思う。冒頭に自分で書いた通り、定義ファイルに実態と同期の取れた構成情報が書かれている状態を作れるわけなので。

もっと時間があれば、例えば Docker コンテナで起動しているサービスイメージを定期的に更新するようなプレイブックも試してみたかったけれど、時間が無いので今回はあきらめる。以下のコマンドで community.docker コレクションを追加した上で、それが提供しているタスクを使えばできるのだと思っている:

ansible-galaxy collection install community.docker

以上

Discussion