複数台のRaspberry Piの初期設定がつらいのでAnsibleで設定した
目次
年末は自宅で稼働する5台のラズパイ達のパッケージアップデートやスクリプト整理などをやっておりました😎
OSのメジャーアップデートもそろそろやらなきゃなということで、ついにbullseyeへアプデすることにしました。 なお既存環境からメジャーアップデートさせるのは非推奨ということらしいので、新たにSDカードにイメージを焼き直して環境構築からやり直そうとしたのですが、これが手順が多くてつらい…。
ということで以前から気になっていたAnsibleで初期設定するようにしてみました。
↓手動で初期設定する記事は以前こちらで書きました。
事前準備
SDカードにOSイメージを焼く
Raspberry Pi Imagerという便利ツールがありますので、これを使ってSDカードにRaspberry Pi OSのイメージを焼きましょう。
↓Raspberry Pi ImagerのDLはこちらからどうぞ。
https://www.raspberrypi.com/software/
なお、Raspberry Pi Imagerの画面上の歯車アイコンを押すとホスト名、Wifi、SSHの初期設定ができるようになっていますので、事前に入力しておきます。
ラズパイでホスト名を設定しておくと、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が発生する為です。
↓参考
https://raspberrypi.stackexchange.com/questions/100598/ufw-and-iptables-on-buster
実行する
インベントリ、Playbook、Rolesが用意できたらあとは実行するのみです。
ansible-playbook -i hosts.ini playbook.ymlでPlaybookの定義を元に処理が実行されます。
あとは気長に処理完了を待つだけです。
あとがき
Ansibleで予め処理セットを準備したおかげでラズパイ初期化への心理的負担がかなり減ったのがメリットだったと思いました!