matuconとマイコン

ITガジェットが好物です

AnsibleでRaspberryPi4の初期設定を自動化

f:id:mazcon:20200811004312p:plain

はじめに

前回インストールした2つのRaspberryPiOSに初期設定を行います。仕事でAnsibleを少し触ったので勉強もかねてAnsibleで自動化してみようと思います。

AnsibleのコントローラマシンはMacを使い、MacからRaspberryPiへプロビジョニング(初期設定)を行います。設定が完了したらBerryBootでイメージ化してバックアップしておくとクリーンな環境への巻き戻しが楽になりそうです。

今回のAnsible導入でメリットと感じた点は以下の通りです。

  • 32bit, 64bit の両OSに対して全く同じ設定を漏れなく反映させることができる
  • 初期設定としてどんな内容を行なったかを設定ファイル(後述のPlaybookファイル) に残しておける
  • 部分的にでも再利用ができそうなので他のOS初期化時にも使えそう
  • Ansible の勉強

自動化する内容

今回は以下の内容で構築しようと思います。

内容 詳細
OS アップデート aptパッケージを最新化しvimufwを追加でインストールします。
vi だとコードのハイライトがデフォルトで効かないのでvimを導入します。
ufwFirewallの設定ツールです。
OS ユーザの作成 作業用のユーザ作成とpi ユーザを無効化します。鍵ファイルも一緒に作成します。
ユーザのグループはpiと同じものを設定しておきます。
自動ログイン修正 OSを起動するとpiユーザで自動ログインされますが、新ユーザでのログインに修正します。
ホスト名変更 複数のOSを判別できるように変更します。
ssh接続はIP管理にするとアドレスを忘れてしまいそうなのでホスト名で接続します。
sshサービス設定 sshはポート変更、パスワードログイン無効化、rootログイン無効化の3点を設定します。
raspi-config の各設定 I2Cの有効化やロケールの設定などを行います。

環境

環境は以下の構成でRaspberryPi はOS インストール直後でネットにつながっている状態から始めます。OSのネットワーク設定はBerryBoot で設定したネットワーク接続情報が自動的に反映されるようです。MacRaspberry Pi は同じネットワークにつなげます。

リソース 内容
本体 Raspberry Pi4 Model B 4GB RAM
OS BerryBoot 上のRaspberry Pi OS 32bit, 64bitの両方
Mac Ansible コントローラマシン

RaspberryPi 側の初期設定

Ansible はsshPython で対象マシンを操作するので、Raspberry Pi側はこの2点だけ設定しておきます。デスクトップで最初に表示される設定ウィザードは「cancel」で閉じておきます。

ssh はraspi-config で有効化します。

「5 Interfacing Options」 > 「P2 SSH」> 「Yes」

Python はすでにインストール済みだったのでパッケージのアップデートだけ実施します。

$ sudo apt updage && sudo apt upgrade

Mac 側 の初期設定

Mac からRaspberry Pissh 接続できるか確認します。接続するのはデフォルトユーザの「pi」でパスワードは「raspberry」です。 同一ネットワークで接続するため、IPの指定はせずホスト名で接続します。

$ ssh pi@raspberrypi.local

Ansible のインストールはhomebrewで行います。作業用フォルダも一緒に作っておきます。

$ brew install ansible
$ mkdir ~/ansible

Ansible の設定

インストールできたらAnsible の設定ファイルを作成します。設定ファイルは主に3種類ありますが、1つはデフォルトの設定であれば不要なので2ファイルだけ作成します。ファイルは上記で作成した「ansible」フォルダ直下に作ります。

種類 ファイル名 内容
Ansible設定ファイル ansible.cfg Ansible 本体の設定です(今回は作らない)
Inventoryファイル inventory.yml (任意の名前) 構成管理対象の接続先などを記述します
Playbookファイル raspi-init.yml (任意の名前) 反映したい内容を記述します

Inventory ファイル

接続先情報は以下の内容で作成しました。先に64bitOSから試してみます。

# 変数定義
all:
  vars:
    user_name: matucon
    host_name: pi464
    ssh_key_name: id_rsa_pi4_64
    modify_ssh_port: 33322

# デフォルトのラズパイ接続先(パスワードでログイン)
pi_default:
  hosts:
    raspberrypi.local:
  vars:
    ansible_ssh_user: pi
    ansible_ssh_pass: raspberry

各対象マシンの環境依存になる値などは変数化が可能です。今回変数化した内容は以下の通りです。(64bitOS版の設定です)

変数 内容
user_name 新しいLinuxユーザ名 matucon
host_name 新しいホスト名 pi464
ssh_key_name 新ユーザのssh鍵ファイル名 id_rsa_pi4_64
modify_ssh_port 変更するsshポート 33322

ファイルができたらAnsible から疎通確認します。

$ ansible pi_default -m ping

以下のようなレスポンスになればOKです。

raspberrypi.local | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "ping": "pong"
}

Playbook ファイル

上記の設定内容をPlaybookファイルに落としたものが以下になります。(長くなってしまったので折りたたみました) タスクごとに日本語コメント入れましたのでご参考ください。

注意点としてネットワークの設定とpiユーザの無効化は一番最後に実施します。Ansibleはpiユーザでsshしてるので、この設定を途中変えるとAnsibleが実行できなくなってしまいます。

Playbookでは新ユーザのパスワードはとりあえずユーザ名と同じにしてるので、設定完了後に変更した方が良さそうです。

ここをクリックしてPlaybook表示

- hosts: pi_default
  become: yes
  become_user: root

  tasks:
    - name: パッケージアップデート
      apt:
        upgrade: dist
    
    - name: パッケージインストール
      apt:
        pkg:
        - vim
        - ufw

    - name: パッケージクレンジング
      apt:
        autoclean: yes
        autoremove: yes

    - name: 新ユーザ作成
      user:
        name: "{{ user_name }}"
        generate_ssh_key: yes
        ssh_key_file: /home/{{ user_name }}/.ssh/{{ ssh_key_name }}
        password: "{{ user_name | password_hash('sha512') }}"
        update_password: on_create

    - name: 公開鍵のファイル名変更
      copy:
        remote_src: true
        src: /home/{{ user_name }}/.ssh/{{ ssh_key_name }}.pub
        dest: /home/{{ user_name }}/.ssh/authorized_keys

    - name: 秘密鍵をローカル(Mac)にコピー
      fetch:
        src: /home/{{ user_name }}/.ssh/{{ ssh_key_name }}
        dest: ~/.ssh/{{ ssh_key_name }}
        flat: yes

    - name: 秘密鍵の権限変更
      file:
        path: /home/{{ user_name }}/.ssh/authorized_keys
        owner: "{{ user_name }}"
        group: "{{ user_name }}"
        mode: '0600'

    - name: .ssh ディレクトリの権限変更
      file:
        path: /home/{{ user_name }}/.ssh/
        state: directory
        owner: "{{ user_name }}"
        group: "{{ user_name }}"
        mode: '0700'
    
    - name: piユーザと同じグループを新ユーザに設定
      shell: |
        usermod -G `groups pi | sed -r "s/^.*: //g" | sed "s/ /,/g"` {{ user_name }}

    - name: piユーザのホームディレクトリを新ユーザのホームディレクトリにコピー
      copy:
        remote_src: true
        src: /home/pi/
        dest: /home/matu/

    - name: sudoers 既存ファイル削除
      file:
        path: /etc/sudoers.d/010_{{ user_name }}-nopasswd
        state: absent

    - name: sudoers 作成
      file:
        path: /etc/sudoers.d/010_{{ user_name }}-nopasswd
        state: touch
        owner: root
        group: root
        mode: 0440

    - name: sudoers 追記
      lineinfile:
        dest: /etc/sudoers.d/010_{{ user_name }}-nopasswd
        state: present
        regexp: '^.*'
        line: "{{ user_name }} ALL=(ALL) NOPASSWD: ALL"

    - name: ssh 設定(パスワードでのログイン無効化)
      lineinfile:
        dest: /etc/ssh/sshd_config
        state: present
        backup: true
        regexp: '^#PasswordAuthentication.*'
        line: 'PasswordAuthentication no'

    - name: ssh 設定(ポート変更)
      lineinfile:
        dest: /etc/ssh/sshd_config
        state: present
        backup: true
        regexp: '^#Port.*'
        line: 'Port {{ modify_ssh_port }}'

    - name: ssh 設定(rootでログイン無効化)
      lineinfile:
        dest: /etc/ssh/sshd_config
        state: present
        backup: true
        regexp: '^#PermitRootLogin.*'
        line: 'PermitRootLogin forced-commands-only'

    - name: 自動ログイン設定
      replace:
        path: /etc/lightdm/lightdm.conf
        regexp: '^autologin-user=pi$'
        replace: 'autologin-user={{ user_name }}'
        backup: true

    - name: 自動ログインサービス設定
      replace:
        path: /etc/systemd/system/autologin@.service
        regexp: '--autologin pi'
        replace: '--autologin {{ user_name }}'
        backup: true

    - name: raspi-config設定
      shell: |
        raspi-config nonint do_hostname {{ host_name }}
        raspi-config nonint do_change_locale ja_JP.UTF-8
        raspi-config nonint do_change_timezone Asia/Tokyo
        raspi-config nonint do_wifi_country JP
        raspi-config nonint do_i2c 0 # yes
        raspi-config nonint do_spi 0 # yes
    
    - name: ホスト名変更
      hostname:
        name: "{{ host_name }}"

    - name: Firewallリセット
      ufw:
        state: reset
        policy: deny
        logging: 'on'

    - name: Firewallルール設定
      ufw:
        rule: allow
        proto: tcp
        src: 192.168.0.0/24 # 接続してるネットワークアドレスを指定
        port: '{{ item }}'
      loop:
        - '80'
        - '443'
        - '8080'
        - '5900'
        - '{{ modify_ssh_port }}'

    - name: Firewall有効化
      ufw:
        state: enabled

    - name: pi ユーザ無効化
      user:
        name: pi
        expires: -1
    
    - name: piユーザパスワード変更
      user:
        name: pi
        password: "{{ user_name | password_hash('sha512') }}"

    - name: piユーザからsudo 権限削除
      shell: gpasswd -d pi sudo

    - name: piユーザのsudo 設定(このplaybook はpiユーザで実行してるので一番最後に実施しないと以降の処理がsudo できずにエラーとなる)
      replace:
        path: /etc/sudoers.d/010_pi-nopasswd
        regexp: '^(pi.*)'
        replace: '# \1' 

Playbook 実行

以下のコマンドでPlaybook を反映します。OSアップデートなど時間のかかる処理があるので気長に待ちます。

$ ansible-playbook raspi-init.yml -i inventory.yml

完了したら手動でOSを再起動します。Ansibleで再起動することもできますが、再起動後のホスト名や接続可能ユーザが変わってしまうので再起動タスクがエラーになってしまいます。

鍵ファイルはMacの以下に出力されてるので、sshログインしてみます。

~/.ssh/id_rsa_pi4_64

以下のコマンドでログインできれば成功です。ポートとssh鍵ファイル名はInventory の変数値です。

$ ssh matucon@pi464.local -p 33322 -i ~/.ssh/id_rsa_pi4_64

確認できたら32bitOSにも反映してみます。設定ファイルの変更箇所は1つだけで、Inventoryの「64」を「32」に変えて実行します。 私の環境ではどちらのOSも上記Playbookで設定することができました。

コマンドが長いので、Macの「~/.ssh/config」に以下を追記しておくと接続が楽になります。

Host pi432
    User matucon
    Port 33322
    HostName pi432.local
    identityFile ~/.ssh/id_rsa_pi4_32
    TCPKeepAlive yes
    IdentitiesOnly yes
Host pi464
    User matucon
    Port 33322
    HostName pi464.local
    identityFile ~/.ssh/id_rsa_pi4_64
    TCPKeepAlive yes
    IdentitiesOnly yes

接続コマンド

$ ssh pi432

途中でエラーが出た場合

今回作ったPlaybookは冪等生が一部ないので同じPlaybookを何度も実行すると既存の設定が壊れてしまう可能性があります。なので問題を直して再実行する場合はエラー直前のタスクまでコメントアウトして実行するかBerryBootでOSResetしてからPlaybookをかけ直しました。

Playbook先頭でやっているOSアップデートは時間がかかるので、アップデートまで完了したものをBerryBootでイメージ化しておくとやり直ししやすくなります。

設定ファイルを編集するタスクであればバックアップが取られているので(ファイル名に日付を入れてバックアップされる)ここから巻き戻すことも可能です。

最後に

AnsibleによるRaspberryPiのプロビジョニングを行いました。Playbook は便宜上1ファイルにまとめてしまいましたが、長くなり過ぎてしまってるので分割したほうがデバッグも管理しやすそうです。