エンジニアの今川(@ug23_)です。
本番環境サーバのユーザ管理、みなさんはどうしていますか?
- みんなで同じユーザ・同じ鍵を使う
- 入社・退職時にはインフラ担当者がユーザ追加・削除する
という感じのレガシーなやり方をしてしまいがちですよね。
全員で同じユーザを使う運用は入社時はとってもラクですが、退職者がでた時、漏洩したかもしれない時、鍵を変えて全員に伝えて…と、無駄が多いですし、サーバ上での動作を監査できるようにするという意味でも、全員がそれぞれ自分のユーザでログインできるようにしておきたいです。
また、人に依存しないしくみにしたいですね。「○○さん休みだからユーザ作れないね」というのはナシにしたいです。
ということでAnsible Playbookでユーザ作成をはじめ、 ユーザの削除 や sudo権限をなくす処理を実行できるようにし、冪等なユーザ管理ができるPlaybookを公開することにしました。
記事内ではリポジトリ内の構成や仕組みを解説しており、記事末尾でリポジトリを掲載していますので参考にしてください。
コードの紹介
基本的な構成
Ansible Playbookにはベストプラクティスが公開されていて、Ansible力の向上のためにこれにのっとってPlaybookを作ろう、と思いました。
分かりづらいところなどは以下を参考にしてください。
ディレクトリ構成
ベストプラクティスのディレクトリ構成にのっとって以下のようになりました。
説明上 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.1
と ansible_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
を用いて暗号化する方法がありますがここでは扱いません。
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の公式ドキュメントに「どんな値をいれるべきか」「どんなオプションがあるのか」が載っています。
今回のポイントは以下の不要ユーザ削除処理です。
- 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_users
とusers
の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できなくなったらこうやって復帰させましょう。
リポジトリ
紹介したコードをGithubの公開リポジトリにおきました!参考にしてください
運用方法
ユーザ登録してほしいひとに自身のPCで ssh-keygen
してもらい、キーペアを作成してもらいます。
ユーザID, 公開鍵を group_vars/all.yml
に記入してもらい、プルリクを出してもらいます。
レビューOKならマージしてAnsible Playbookを実行、という流れでやっています。
現状はできていませんが、マージ後即Playbook実行というのもできますね!
まとめ
Ansible Playbookのベストプラクティスとされているディレクトリ構成で、ユーザの登録・削除を行うPlaybookを作成しました。
具体的には以下の条件を満たすPlaybookを作成しました。
- ユーザの作成・削除を冪等に行う
- 設定ファイルにユーザ情報を追加したらユーザが追加される
- 設定ファイルから消えたらそのユーザはいないようにする
- sudoersの管理もやる
- Ansible PlaybookのベストプラクティスのDirectory Layoutにしたがう
ベストプラクティスに従うのは最初の習得コストがかかるものの「どこになにを置くか」が明確になるので最初に整えておきたいですね。
春は出会いと別れの季節、それらがつらいものにならないように普段からラクできる構成にしておきたいですね!