Ansible Playbookで連番の複数ユーザーを一括作成する

# はじめに

Ansible Playbookでユーザーを複数作成する場合、あらかじめリスト変数にユーザー名、UID、グループ名等々を設定しておき、userモジュール＋ループで一括作成、というのが通常かと思います。

ただ、あまりに大量のユーザーを作成する場合、そのリスト変数を用意するのが面倒です。

連番のユーザーをサクッと作成するPlaybookがあればいいのに、と思っていたところ、ようやくそれなりのものが出来たので、記念に残します。


# 想定ケース

例えば開発者向けなど、連番のユーザーを大量に作成する。

# 環境

VirtualBox (+ vagrant)
CentOS 7.7
ansible 2.9.6

# Playbook

今回も記事を分かりやすくするため、1本のPlaybook`usercreate.yml`に全て記載します。（ベストプラクティスを知らないわけではないです。念の為。）

まず冒頭で、各種変数を準備します。この例では、01から20までの連番のユーザーを作成します。詳しい説明はコメントを参照ください。

```yaml:usercreate.yml
---
- name: Create many user
  hosts: localhost
  vars:
    # 1から20までをカンマで繋いだ文字列を作ります。最後はカンマで終わりますが、後でなんとかします。
    user_num_data: "{% for n in range(20) %}{{n+1}},{% endfor %}"
    # 上で作った変数をカンマで分割して、リスト変数に登録します。最後の要素は空ですが、後でなんとかします。
    user_num: "{{ user_num_data.split(',') }}"
    # 作成するユーザーの初期パスワードです
    initialpw: zaq12wsx
    # UID/GIDのプレフィックスです。5000番台を使います。
    pre_id: 50
    # ユーザー名のプレフィックスです。
    pre_name: devuser
    # 作成するユーザー全員を登録する共通グループです。
    common_group: devgroup
```

続いてタスク定義です。まずは共通グループを作成します。ユーザーで使用しない0番、GID=5000で作成します。

```yaml:usercreate.yml
  tasks:
    - name: Common group is created
      group:
        name: '{{ common_group }}'
        gid: '{{ pre_id }}00'
        state: present
```

次は作成するユーザーごとのグループを作成します。ループするリスト変数を`'{{ user_num[:-1] }}'`とすることで、最後の空要素を除外します。そして、ユーザー名などに連番を使用する際、2桁に揃えるために、`item.zfill(2)`とします。

```yaml:usercreate.yml
    - name: Groups are created
      group:
        name: '{{ pre_name }}{{ item.zfill(2) }}'
        gid: '{{ pre_id }}{{ item.zfill(2) }}'
        state: present
      with_items:
        - '{{ user_num[:-1] }}'
```

最後にユーザーを作成します。

```yaml:usercreate.yml
    - name: Users are created
      user:
        name: '{{ pre_name }}{{ item.zfill(2) }}'
        group: '{{ pre_name }}{{ item.zfill(2) }}'
        groups: '{{ pre_name }}{{ item.zfill(2) }}, {{ common_group }}'
        uid: '{{ pre_id }}{{ item.zfill(2) }}'
        state: present
        password: "{{ initialpw | password_hash('sha512') }}"
        update_password: on_create
      with_items:
        - '{{ user_num[:-1] }}'
      register: usercreated
```

おまけで、初期パスワードの変更を促すために、パスワードを無効にします。前のタスクで、ユーザーが作成された時にのみ実行されるようにwhenで条件を指定します。あと、ansible-playbook実行時の出力を抑えるために、loop_controlのlabelを指定します。

```yaml:usercreate.yml
    - name: Passwords are expired
      shell: |
        passwd -e '{{ item.invocation.module_args.name }}'
      with_items:
        - '{{ usercreated.results }}'
      when:
        - item.changed
        - item.invocation.module_args.state == "present"

      loop_control:
        label: "{{ item.invocation.module_args.name }}"
```

# 実行例

普通の実行結果です。

```
$ ansible-playbook -i inventories/test usercreate.yml 

PLAY [Create many user] ************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************
ok: [localhost]

TASK [Common group is created] *****************************************************************************************
changed: [localhost]

TASK [Groups are created] **********************************************************************************************
changed: [localhost] => (item=1)
changed: [localhost] => (item=2)
changed: [localhost] => (item=3)
changed: [localhost] => (item=4)
changed: [localhost] => (item=5)
changed: [localhost] => (item=6)
changed: [localhost] => (item=7)
changed: [localhost] => (item=8)
changed: [localhost] => (item=9)
changed: [localhost] => (item=10)
changed: [localhost] => (item=11)
changed: [localhost] => (item=12)
changed: [localhost] => (item=13)
changed: [localhost] => (item=14)
changed: [localhost] => (item=15)
changed: [localhost] => (item=16)
changed: [localhost] => (item=17)
changed: [localhost] => (item=18)
changed: [localhost] => (item=19)
changed: [localhost] => (item=20)

TASK [Users are created] ***********************************************************************************************
changed: [localhost] => (item=1)
changed: [localhost] => (item=2)
changed: [localhost] => (item=3)
changed: [localhost] => (item=4)
changed: [localhost] => (item=5)
changed: [localhost] => (item=6)
changed: [localhost] => (item=7)
changed: [localhost] => (item=8)
changed: [localhost] => (item=9)
changed: [localhost] => (item=10)
changed: [localhost] => (item=11)
changed: [localhost] => (item=12)
changed: [localhost] => (item=13)
changed: [localhost] => (item=14)
changed: [localhost] => (item=15)
changed: [localhost] => (item=16)
changed: [localhost] => (item=17)
changed: [localhost] => (item=18)
changed: [localhost] => (item=19)
changed: [localhost] => (item=20)

TASK [Passwords are expired] *******************************************************************************************
changed: [localhost] => (item=devuser01)
changed: [localhost] => (item=devuser02)
changed: [localhost] => (item=devuser03)
changed: [localhost] => (item=devuser04)
changed: [localhost] => (item=devuser05)
changed: [localhost] => (item=devuser06)
changed: [localhost] => (item=devuser07)
changed: [localhost] => (item=devuser08)
changed: [localhost] => (item=devuser09)
changed: [localhost] => (item=devuser10)
changed: [localhost] => (item=devuser11)
changed: [localhost] => (item=devuser12)
changed: [localhost] => (item=devuser13)
changed: [localhost] => (item=devuser14)
changed: [localhost] => (item=devuser15)
changed: [localhost] => (item=devuser16)
changed: [localhost] => (item=devuser17)
changed: [localhost] => (item=devuser18)
changed: [localhost] => (item=devuser19)
changed: [localhost] => (item=devuser20)

PLAY RECAP *************************************************************************************************************
localhost                  : ok=5    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

/etc/passwdを見ると、綺麗に連番で作成されています。

```
$ grep devuser /etc/passwd
devuser01:x:5001:5001::/home/devuser01:/bin/bash
devuser02:x:5002:5002::/home/devuser02:/bin/bash
devuser03:x:5003:5003::/home/devuser03:/bin/bash
devuser04:x:5004:5004::/home/devuser04:/bin/bash
devuser05:x:5005:5005::/home/devuser05:/bin/bash
devuser06:x:5006:5006::/home/devuser06:/bin/bash
devuser07:x:5007:5007::/home/devuser07:/bin/bash
devuser08:x:5008:5008::/home/devuser08:/bin/bash
devuser09:x:5009:5009::/home/devuser09:/bin/bash
devuser10:x:5010:5010::/home/devuser10:/bin/bash
devuser11:x:5011:5011::/home/devuser11:/bin/bash
devuser12:x:5012:5012::/home/devuser12:/bin/bash
devuser13:x:5013:5013::/home/devuser13:/bin/bash
devuser14:x:5014:5014::/home/devuser14:/bin/bash
devuser15:x:5015:5015::/home/devuser15:/bin/bash
devuser16:x:5016:5016::/home/devuser16:/bin/bash
devuser17:x:5017:5017::/home/devuser17:/bin/bash
devuser18:x:5018:5018::/home/devuser18:/bin/bash
devuser19:x:5019:5019::/home/devuser19:/bin/bash
devuser20:x:5020:5020::/home/devuser20:/bin/bash
```

/etc/groupも同様。

```
$ grep dev /etc/group
devgroup:x:5000:devuser01,devuser02,devuser03,devuser04,devuser05,devuser06,devuser07,devuser08,devuser09,devuser10,devuser11,devuser12,devuser13,devuser14,devuser15,devuser16,devuser17,devuser18,devuser19,devuser20
devuser01:x:5001:devuser01
devuser02:x:5002:devuser02
devuser03:x:5003:devuser03
devuser04:x:5004:devuser04
devuser05:x:5005:devuser05
devuser06:x:5006:devuser06
devuser07:x:5007:devuser07
devuser08:x:5008:devuser08
devuser09:x:5009:devuser09
devuser10:x:5010:devuser10
devuser11:x:5011:devuser11
devuser12:x:5012:devuser12
devuser13:x:5013:devuser13
devuser14:x:5014:devuser14
devuser15:x:5015:devuser15
devuser16:x:5016:devuser16
devuser17:x:5017:devuser17
devuser18:x:5018:devuser18
devuser19:x:5019:devuser19
devuser20:x:5020:devuser20
```

パスワードもexpireされてます。

```
$ sudo chage -l devuser01
Last password change					: password must be changed
Password expires					: password must be changed
Password inactive					: password must be changed
Account expires						: never
Minimum number of days between password change		: 0
Maximum number of days between password change		: 99999
Number of days of warning before password expires	: 7
```

ユーザーが足らなくなったら、以下の`range(20)`のところを`range(40)`などとして、Playbookを再実行すれば、作成済みのユーザーには影響なく、追加分のみ作成してくれます。

```yaml
user_num_data: "{% for n in range(20) %}{{n+1}},{% endfor %}"
```

これで、数十人のメンバーがいるチームが複数立ち上がっても、ドンとこい！です。