AnsibleでLinux初期ログイン時のパスワード変更からIPアドレス変更までの初期設定をする

久々にラズパイにUbuntuを入れて触ろうとしたんですが、毎回初期設定がめんどいですね。
SDカードを焼いて、ラズパイに挿して、LANケーブル挿して、電源を入れる。
その後Ansible Playbookを実行すると固定IPを設定するまでやってくれるようなやつを作りました。
完全に自分用なので、Ubuntu以外(さらにはUbuntu 22.04 以外)では動かない、かなり狭い守備範囲のPlaybookです。
(過去に似たような記事を書いたんですが、それのアップデートっぽい記事です)

https://github.com/kefi550/ansible-raspi

ラズパイ(その他Linux on SBC)の初期設定をAnsibleでやる場合の課題

ラズパイなどシングルボードにLinux入れるときは基本的には、ディスプレイ、キーボードつなげて初回ログインすると思います。これが一番個人的にはめんどく、Ansibleによって自動化したいステップです。
AnsibleでLinuxの初期設定をやるときの主な課題は以下です。

  • 初回ログイン時にパスワードリセットを強制される
  • DHCPアドレスから固定IPアドレスに変更したときにAnsibleとして接続が途切れてplaybookを継続できなくなる

これらに絞って紹介します。

初回ログイン時のパスワードリセット対応

初回ログインか(パスワードリセットが必要か)の判定

まずタスクはこんな感じ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
- name: ping inventory_hostname
      ping:
      ignore_unreachable: true
      register: ping_inventory_hostname

    - name: ping to ip
      block:
        - name: set static_address, new_user
          set_fact:
            ansible_user: "{{ users[0].name }}"
            ansible_host: "{{ static_address }}"
          when:
            - ping_inventory_hostname.unreachable is defined
            - ping_inventory_hostname.unreachable == true

        - name: ping static_ip
          ping:
          ignore_unreachable: true
          register: ping_static_ip

        # static_ip への pingが成功しなかったら、dhcp_ip, default_userを使う
        - name: set dhcp_address, default_user
          set_fact:
            ansible_user: "{{ default_user }}"
            ansible_password: "{{ default_user_password }}"
            ansible_host: "{{ dhcp_address }}"
          when:
            - ping_static_ip.unreachable is defined
            - ping_static_ip.unreachable == true

        - name: ping dhcp_ip
          ping:
          become: true
          become_user: "{{ default_user }}"
          ignore_unreachable: true
          register: ping_dhcp_ip
          failed_when:
            - ping_dhcp_ip.module_stderr is defined
            - '"Your password has expired" not in ping_dhcp_ip.module_stderr'
          when:
            - ping_static_ip.unreachable is defined
            - ping_static_ip.unreachable == true
      when:
        - ping_inventory_hostname.unreachable is defined
        - ping_inventory_hostname.unreachable == true

    - name: check password init is required
      set_fact:
        password_init_required: "{{ ping_dhcp_ip.module_stderr is defined and  'Your password has expired' in ping_dhcp_ip.module_stderr }}"
                                

    - name: password init
      include_role:
        name: password_init
      vars:
        password_init_host: "{{ ansible_host }}"
        password_init_default_user: "{{ default_user }}"
        password_init_default_user_password: "{{ default_user_password }}"
      when:
        password_init_required == true

pingモジュールを使いました。
pingモジュールは ssh の疎通をチェックするモジュールです。
ubuntuの初期ログイン時は Your Password has expired というようなメッセージとともに、pingモジュールとして失敗するようです。

そこでパスワードリセットが必要かどうかの判定は以下です。

  • inventory_hostname (基本的にはDNS的な “名前” を想定) に対してssh疎通をチェックする -> このssh疎通ができたらパスワードリセットは必要ない
  • static_ip (設定したい固定IPアドレス) に対してssh疎通をチェックする -> このssh疎通ができたらパスワードリセットは必要ない
  • dhcp_ip に対してssh疎通をチェックする -> パスワードリセット済みならチェック合格、リセット必要ならチェック不合格になる

pingモジュールは宛先ホストを設定するようなパラメータはありません。
なので、ansible_host 変数を set_fact を使って書き換えながら判定フローを回しています。

初期パスワードの変更

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
- name: Change password on initial login
  delegate_to: 127.0.0.1
  expect:
    command: ssh {{ ssh_args }} {{ password_init_default_user }}@{{ password_init_host }}
    timeout: 10
    responses:
      "(?i)@.*password": "{{ password_init_default_user_password }}"
      "(?i)Current Password": "{{ password_init_default_user_password }}"
      "(?i)New password": "{{ tmp_password }}"
      "(?i)Retypr new password": "{{ tmp_password }}"
  register: change_password_result

- name: change ssh password
  set_fact:
    ansible_ssh_pass: "{{ tmp_password }}"

以前の記事 でも書いたことですが、このようなタスクになりました。

expectモジュールを使ってパスワードリセットのプロンプトに対応しています。
また、パスワードリセット完了後にplaybookの処理を継続させるために、ansible_ssh_pass 変数を set_fact で新しいパスワード(playbook実行完了までの一時パスワード)に変更しています

IPアドレスを変更したあともplaybookの処理を継続させる

固定IPアドレスを設定しようとすると、基本的には接続先IPが変わることでAnsibleがリモートホストに再接続できなくなり、playbookが継続できなくなります。
今回はnetplanを使って固定IPの設定をしていて、以下のようなタスクになりました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
---
- name: configure netplan
  template:
    src: netplan_config.yaml.j2
    dest: /etc/netplan/config.yaml
  become: true
  register: configure_netplan

- name: netplan apply
  block:
    - name: netplan apply
      become: true
      command: netplan apply
      async: 300
      poll: 0
      when: configure_netplan.changed
      register: netplan_apply_result

    - name: set ansible_host to static_ip
      set_fact:
        ansible_host: "{{ static_address }}"

    - name: wait for connection to new ip
      wait_for_connection:
        delay: 10
        timeout: 60
      when: configure_netplan.changed

基本的には netplan apply でIPアドレスが変わって、再接続できなくなり、playbookが止まります。
今回は async, poll を使ってタスクを非同期にすることで処理を継続できるようにしました。
netplan applyタスクを非同期で開始した直後に ansible_host 変数を固定IP(新しいIP)に変更します。
その後、wait_for_connection を使って、新しいIPアドレスへの疎通を確認させます。

まとめ

Ansibleで出来たてのUbuntuの初期設定をスムーズにやる方法を紹介しました。
初期パスワードリセットと、IPアドレス変更がめんどいとこだと個人的には思うので、一旦いい感じのplaybookができてよかったです。