エンジニアのはしがき

プログラミングの日々の知見を書き連ねているブログです

複数台のRaspberry Piの初期設定がつらいのでAnsibleで設定した

年末は自宅で稼働する5台のラズパイ達のパッケージアップデートやスクリプト整理などをやっておりました😎

OSのメジャーアップデートもそろそろやらなきゃなということで、ついにbullseyeへアプデすることにしました。 なお既存環境からメジャーアップデートさせるのは非推奨ということらしいので、新たにSDカードにイメージを焼き直して環境構築からやり直そうとしたのですが、これが手順が多くてつらい…。

ということで以前から気になっていたAnsibleで初期設定するようにしてみました。

↓手動で初期設定する記事は以前こちらで書きました。

tm-progapp.hatenablog.com

事前準備

SDカードにOSイメージを焼く

Raspberry Pi Imagerという便利ツールがありますので、これを使ってSDカードにRaspberry Pi OSのイメージを焼きましょう。

Raspberry Pi ImagerのDLはこちらからどうぞ。

www.raspberrypi.com

なお、Raspberry Pi Imagerの画面上の歯車アイコンを押すとホスト名、WifiSSHの初期設定ができるようになっていますので、事前に入力しておきます。 ラズパイでホスト名を設定しておくと、IPではなく{ホスト名}.localで接続できるので便利です。

初期設定をしておくと、いちいちモニタやキーボードをラズパイに繋いでWifi, SSHの初期設定をする必要もなくなります。(知らないうちにソフトがかなり便利になっていてうれしいですね)

ちょっとハマったこと

Raspberry Pi Zero WはWifiの5GHz帯未対応なので、2.4GHz帯のWifiに接続するようにしましょう…。これに気付かず数時間溶かしました🤤

公開鍵認証でSSH接続できるようにする

AnsibleはSSHでコマンド実行するので予め公開鍵認証でSSH接続できるように設定します。

以下はSSH接続するホストでのコマンド例です。

# SSH用の公開鍵・秘密鍵を生成する
$ ssh-keygen

# デフォルトだとラズパイに.sshディレクトリがないので作成する
$ ssh {接続先のホスト名} mkdir -p /home/pi/.ssh

# 公開鍵をラズパイのauthorized_keysに渡す
$ cat "${HOME}/.ssh/id_rsa.pub" | ssh {接続先のホスト名} "cat >> /home/pi/.ssh/authorized_keys"

# sshd_configのパスワード認証をOFF、authorized_keysのパスを指定する
$ echo 'PasswordAuthentication no' | ssh {接続先のホスト名} "sudo tee -a /etc/ssh/sshd_config"
$ echo 'AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2' | ssh {接続先のホスト名} "sudo tee -a /etc/ssh/sshd_config"

これで公開鍵認証でSSH接続できるようになりました。

Ansibleを使う

Ansibleをインストールする

aptでインストールすると2.10.x以上のAnsibleがインストールできなかったので、pipでインストールしています。

# Ansibleのインストール
$ python3 -m pip install --user ansible

# インストールされたか確認する
$ ansible --version
ansible [core 2.13.7]
  config file = /home/tm/ansible/ansible-raspberry-pi/ansible.cfg
  configured module search path = ['/home/tm/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/tm/.local/lib/python3.8/site-packages/ansible
  ansible collection location = /home/tm/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/tm/.local/bin/ansible
  python version = 3.8.10 (default, Nov 14 2022, 12:59:47) [GCC 9.4.0]
  jinja version = 3.1.2
  libyaml = True

# community.generalはufwを扱う際に必要になるのでインストールします
$ ansible-galaxy collection install community.general

ディレクトリ構造

今回、下記のようなディレクトリ構造にしました。

$ tree
.
├── ansible.cfg
├── hosts.ini
├── playbook.yml
├── roles
│   ├── apt
│   │   ├── tasks
│   │   │   └── main.yml
│   ├── git
│   │   ├── tasks
│   │   │   └── main.yml
│   ├── nvm
│   │   ├── tasks
│   │   │   └── main.yml
│   ├── osinfo
│   │   ├── tasks
│   │   │   └── main.yml
│   ├── ssh
│   │   ├── tasks
│   │   │   └── main.yml
│   ├── ufw
│   │   ├── tasks
│   │   │   └── main.yml
│   └── vim
│   │   ├── tasks
│   │   │   └── main.yml

Ansibleにはインベントリ、Playbook、Rolesといった固有の概念があります。

  • ansible.cfg: 設定ファイル。ログ出力パス等を定義する。
  • hosts.ini: インベントリファイル。対象ホストを定義する。
  • playbook.yml: Playbook。実行する命令や実行時の条件を定義する。
  • roles/*: Rolesの定義。Playbookから呼び出せる処理の固まり。

インベントリー(hosts.ini)

ファイル名は任意です。

[raspiservers]
mustang.local ansible_user=pi
jaguar.local ansible_user=pi
stratocaster.local ansible_user=pi
lespaul.local ansible_user=pi
telecaster.local ansible_user=pi

今回操作対象となるホスト名と接続時に使うユーザー名をansible_userに指定します。

ちなみに我が家のラズパイにはギター由来の名前を付いていてそれがホスト名になっています🎸

Playbook(playbook.yml)

ファイル名は任意です。

---
- hosts: raspiservers
  become: yes
  become_user: root
  roles:
    - apt
    - git
    - vim
    - nvm
    - ssh
    - ufw

hostsにはhosts.iniで定義しておいたセクションを指定できます。

apt installなどでroot権限が必要になってくるので、予めbecome: yes, become_user: rootでrootユーザーで実行するようにしています。

rolesには後述しますがRolesで定義した具体的な処理の固まりを呼び出しています。

playbook.yml自体に具体的な処理(apt update, apt installにあたる処理など)を書くことも可能ですが、予めRolesに処理を切り出しておくことで他のPlaybookから再利用できたり、長期的にコードが煩雑になり辛くなるので分けておく方が良いようです。

設定ファイル(ansible.cfg)

[defaults]
log_path=/tmp/ansible.log

log_pathでログ出力先を指定しています。

Roles(roles/*)

roles配下にRole別にディレクトリを作成し、roles/*/tasks/main.ymlに具体的な処理を記述します。

新規にRoleを追加したい時はansible-galaxy init roles/{Role名}を叩くと必要なファイル群を自動生成してくれるので便利です。

roles/apt/tasks/main.yml

---
- name: apt update
  apt:
    update_cache: yes
- name: apt upgrade
  apt:
    upgrade: yes
- name: install apt packages
  apt:
    name:
      - vim
      - git
      - tig
      - ufw
      - chromium
      - jq

roles/git/tasks/main.yml

---
- name: check gitconfig(global) already exist
  become: no
  stat:
    path: /home/pi/.gitconfig
  register: gitconfig_global
  changed_when: not gitconfig_global.stat.exists

- name: configure git global
  become: no
  shell: |
    git config --global user.name "hogefuga"
    git config --global user.email "hogefuga@gmail.com"
  when: not gitconfig_global.stat.exists

git config --globalがいつも面倒なのでここでセットしています。

roles/vim/tasks/main.yml

---
- name: check vimrc already exist
  stat:
    path: /home/pi/.vimrc
  register: vimrc
  changed_when: not vimrc.stat.exists

- name: configure vim
  shell: |
    echo >> 'syntax on' /home/pi/.vimrc
    echo >> 'set tabstop=4' /home/pi/.vimrc
    echo >> 'set number' /home/pi/.vimrc
    echo >> 'set hlsearch' /home/pi/.vimrc
    update-alternatives --set editor /usr/bin/vim.basic
  when: not vimrc.stat.exists

.vimrcによく指定するパラメータをセットしています。

roles/nvm/tasks/main.yml

---
- name: check nvm already installed
  stat:
    path: /home/pi/.nvm
  register: result
  changed_when: not result.stat.exists

- name: install nvm
  become: no
  shell: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
  when: not result.stat.exists

- name: nvm install nodejs
  become: no
  shell: source /etc/profile && nvm install --lts
  # nvm install is too late for Raspberry Pi Zero...
  timeout: 300
  when: not result.stat.exists

Node.jsはバージョン管理したいケースが多いのでnvmをインストールさせました。

timeoutを指定すると処理が指定した秒数でタイムアウトするようになります。

Raspberry Pi Zeroだと恐らくスペックが低いが故にフリーズ状態になってしまう時があったので、タイムアウトさせるようにしました。

roles/ssh/tasks/main.yml

---
- name: check id_rsa exist
  stat:
    path: /home/pi/.ssh/id_rsa
  register: idrsa
  changed_when: not idrsa.stat.exists

- name: ssh-keygen
  become: no
  shell: |
    ssh-keygen -b 2048 -t rsa -f /home/pi/.ssh/id_rsa -q -N ""
  when: not idrsa.stat.exists

GitHubに公開鍵を登録することも多いので予めSSHの公開鍵・秘密鍵を生成させています。

roles/ufw/tasks/main.yml

---
- name: ufw already set
  shell: ufw status
  register: ufw_status
  changed_when: ufw_status.rc >= 1

- name: reboot fro ufw
  ansible.builtin.reboot: reboot_timeout=300
  when: ufw_status.rc >= 1

- name: enable ufw
  community.general.ufw:
    state: enabled
    policy: deny
  when: ufw_status.rc >= 1

- name: enable ufw logging
  community.general.ufw:
    logging: 'on'
  when: ufw_status.rc >= 1

- name: ufw allow tcp port 22
  community.general.ufw:
    rule: allow
    port: '22'
    proto: tcp
  when: ufw_status.rc >= 1

- name: ufw allow tcp port 5900
  community.general.ufw:
    rule: allow
    port: '5900'
    proto: tcp
  when: ufw_status.rc >= 1

ansible.builtin.rebootでホストが再起動します。

再起動をあえて挟んでいる理由は、そのままufw enableをしようとするとERROR: Couldn't determine iptables versionが発生する為です。

↓参考

pi 4 - ufw and iptables on buster - Raspberry Pi Stack Exchange

実行する

インベントリ、Playbook、Rolesが用意できたらあとは実行するのみです。

ansible-playbook -i hosts.ini playbook.ymlでPlaybookの定義を元に処理が実行されます。

あとは気長に処理完了を待つだけです。

あとがき

Ansibleで予め処理セットを準備したおかげでラズパイ初期化への心理的負担がかなり減ったのがメリットだったと思いました!