SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

Ansible Playbookでユーザ管理(登録・削除)をまるっとやる

エンジニアの今川(@ug23_)です。

本番環境サーバのユーザ管理、みなさんはどうしていますか?

  • みんなで同じユーザ・同じ鍵を使う
  • 入社・退職時にはインフラ担当者がユーザ追加・削除する

という感じのレガシーなやり方をしてしまいがちですよね。

全員で同じユーザを使う運用は入社時はとってもラクですが、退職者がでた時、漏洩したかもしれない時、鍵を変えて全員に伝えて…と、無駄が多いですし、サーバ上での動作を監査できるようにするという意味でも、全員がそれぞれ自分のユーザでログインできるようにしておきたいです。

また、人に依存しないしくみにしたいですね。「○○さん休みだからユーザ作れないね」というのはナシにしたいです。

ということでAnsible Playbookでユーザ作成をはじめ、 ユーザの削除sudo権限をなくす処理を実行できるようにし、冪等なユーザ管理ができるPlaybookを公開することにしました。

記事内ではリポジトリ内の構成や仕組みを解説しており、記事末尾でリポジトリを掲載していますので参考にしてください。

コードの紹介

基本的な構成

Ansible Playbookにはベストプラクティスが公開されていて、Ansible力の向上のためにこれにのっとってPlaybookを作ろう、と思いました。

分かりづらいところなどは以下を参考にしてください。

docs.ansible.com

ディレクトリ構成

ベストプラクティスのディレクトリ構成にのっとって以下のようになりました。

説明上 tree コマンドの出力をちょっといじっています。

. 
├── production
├── staging
├── ansible.cfg
├── group_vars
│   └── all.yml
├── internal_servers.yml
├── main.yml
├── management.yml
├── roles
│   ├── common
│   │   └── tasks
│   │       ├── main.yml
│   └── sudoers
│       └── tasks
│           └── main.yml
└── README.md

情報のもたせ方としては以下のような棲み分けがされています

  • Inventoryファイル(production, staging): 管理対象のホスト名・環境依存の変数
  • group_vars: ホストのグループごとの変数(今回はユーザ名等)
  • main.yml: (internal_servers|management).yml を呼びだす
  • (internal_servers|management).yml: どのホストにどのロールを割り当てるかを定義
  • roles/[ロール名]/tasks/main.yml: 実際にロールが割り当てられたときに実行される処理の定義

実際に本番環境へユーザの変更を適用する場合は以下のようなコマンドを打っています。

ansible-playbook main.yml -i production -f 5 --private-key secret.key

-f 5 は並列実行オプション、 --private-key で全サーバ共通の秘密鍵を指定しています。さらに込み入った設定をする場合は ansible.cfg を用います。

Inventoryファイル(production, staging)

[app]
app1
app2

[app:vars]
ansible_ssh_user=ec2-user

[management]
management

[management:vars]
ansible_ssh_user=ubuntu

[analysis]
analysis1
analysis2

[analysis:vars]
ansible_ssh_user=ubuntu

[jenkins]
127.0.0.1

[jenkins:vars]
ansible_connection=local

Ansibleを適用するホストをグループごとにまとめて定義します。ホスト名はAnsible Playbookを実行するサーバから到達可能なホスト名であればOKです。IPアドレスも可能です。 グループごとにvarsを定義できます。ここでは環境ごとにAmazon LinuxとUbuntuが混在しています。

localhostに向けて適用したい場合は jenkins のように 127.0.0.1ansible_connection=local を指定してください。

また、productionとstagingを分けているように、環境による差異はこの2つで区別します。

環境によって差し替えたい値が多い場合Best Practiceで紹介されている Alternative Directory Layout を利用することをおすすめします。

group_vars

Inventoryで定義したグループごとにvarsを定義できます。

今回は全体で使うので all のみ定義しています

---
users:
  - name: 'ug23'
    public_key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABA...'
sudoers:
  - 'ug23'

admin_group: 'admin'
user_group: 'developers'

ユーザIDと公開鍵をここに配置します。パスワードなしでsudoを許可するユーザのユーザIDをsudoers配下に入れます。 ここには公開鍵が載ります。よりセキュアにしたい場合、このファイルを ansible-vault を用いて暗号化する方法がありますがここでは扱いません。

qiita.com

main.yml, internal_servers.yml, management.yml

実際にホストにたいして行われる処理を記述するYAMLファイルを見ていきます。

main.yml

---
- import_playbook: internal_servers.yml
- import_playbook: management.yml

internal_servers.yml

---
- hosts: app analysis jenkins
  become: True
  roles:
    - common
    - sudoers

management.yml

---
- hosts: management
  become: True
  roles:
    - common

「どのホストに」「どのロールを適用するか」を定義します。 roles につけるロールをコントロールすることで適用したい処理を選択することができます。

roles

roles配下にロール名でディレクトリを切ることでロールを表現します。

tasks/main.yml が最初に呼ばれるので基本的にはここに書いた処理が実行されます。

ロール: common

全体に適用する処理を定義しています。

---
- name: ユーザグループの作成
  group: name={{ user_group }}

- name: ユーザ作成とグループ登録
  user: >
    name={{ item.name }}
    group={{ user_group }}
    groups={{ user_group }}
  with_items:  '{{ users }}'

- name: ユーザ作成とグループ登録
  user: >
    name={{ item.name }}
    group={{ user_group }}
    groups={{ user_group }}
  with_items:  '{{ users }}'

# Amazon Linux上でauthorized_keyモジュールを実行するのに必須
- name: libselinux-pythonインストール
  yum: name=libselinux-python state=present
  when: "ansible_distribution == 'Amazon'"

- name: ユーザごとの公開鍵の登録
  authorized_key: >
    user={{ item.name }}
    key={{ item.public_key }}
  with_items:  '{{ users }}'

- name: インスタンス上のユーザリストの取得
  shell: 'getent group developer | cut -d: -f4 | tr "," "\n"'
  register: current_users

- name: usersにないユーザを削除
  user: >
    name={{ item }}
    state=absent
    remove=yes
  with_items: "{{ current_users.stdout_lines | difference(users | map(attribute='name') | list) }}"

- name から始まる各要素が1つのtaskを示しています。

{{ user_group }} などのように文字列中で二重braceでくくることで変数展開できます。

group user yum などはAnsibleのモジュールであり、Ansibleの公式ドキュメントに「どんな値をいれるべきか」「どんなオプションがあるのか」が載っています。

docs.ansible.com

今回のポイントは以下の不要ユーザ削除処理です。

- name: インスタンス上のユーザリストの取得
  shell: 'getent group developer | cut -d: -f4 | tr "," "\n"'
  register: current_users

- name: usersにないユーザを削除
  user: >
    name={{ item }}
    state=absent
    remove=yes
  with_items: "{{ current_users.stdout_lines | difference(users | map(attribute='name') | list) }}"
  • 実際に存在しているユーザのリストを行区切りで取得し、 current_users に保存
  • current_usersusers のnameだけのリストを用意
  • 上記リストの差集合をとり、current_usersにだけ多く存在するユーザを削除

という処理を行っています。

Ansibleでなんらかの削除処理をする場合 実際の値を取得して 存在してはいけない要素を削除する、という流れでやれば「作りっぱなしのまま残ってしまう」を防ぐことができます。

ロール: sudoers

---
- name: 管理者グループの作成
  group: name={{ admin_group }}

- name: 管理者グループをsudoersに追加
  lineinfile:
    path: /etc/sudoers
    state: present
    regexp: "%{{ admin_group }}"
    line: "%{{ admin_group }} ALL=(ALL) NOPASSWD: ALL"

- name: 管理者ユーザを管理者グループに追加
  user: >
    name={{ item }}
    group={{ admin_group }}
    groups={{ user_group }},{{ admin_group }}
  with_items:  '{{ sudoers }}'

- name: 実際に管理者グループに属するユーザのリストを取得
  shell: 'getent group {{ admin_group }} | cut -d: -f4 | tr "," "\n"'
  register: present_sudoers

- name: sudoersに存在しないユーザを管理者グループから外す
  user: >
    name={{ item }}
    groups={{ user_group }}
  with_items: '{{  present_sudoers.stdout_lines | difference(sudoers) }}'
  ignore_errors: yes # ユーザが削除済みの場合があるため

sudoersの編集は安全な環境で試したほうがよいです。
visudo ではsudoersファイルの文法チェックをやってくれるのですが、 lineinfile モジュールを使った編集では文法チェックが走りません。私はこれをミスって本番環境にsudoできなくさせました。気をつけましょう。

sudoできなくなったらこうやって復帰させましょう。

qiita.com

リポジトリ

紹介したコードをGithubの公開リポジトリにおきました!参考にしてください

github.com

運用方法

ユーザ登録してほしいひとに自身のPCで ssh-keygen してもらい、キーペアを作成してもらいます。

ユーザID, 公開鍵を group_vars/all.yml に記入してもらい、プルリクを出してもらいます。

レビューOKならマージしてAnsible Playbookを実行、という流れでやっています。

現状はできていませんが、マージ後即Playbook実行というのもできますね!

まとめ

Ansible Playbookのベストプラクティスとされているディレクトリ構成で、ユーザの登録・削除を行うPlaybookを作成しました。

具体的には以下の条件を満たすPlaybookを作成しました。

  • ユーザの作成・削除を冪等に行う
    • 設定ファイルにユーザ情報を追加したらユーザが追加される
    • 設定ファイルから消えたらそのユーザはいないようにする
  • sudoersの管理もやる
  • Ansible PlaybookのベストプラクティスのDirectory Layoutにしたがう

ベストプラクティスに従うのは最初の習得コストがかかるものの「どこになにを置くか」が明確になるので最初に整えておきたいですね。

春は出会いと別れの季節、それらがつらいものにならないように普段からラクできる構成にしておきたいですね!