ansible-ldap is very simple and useful

OpenLDAP と仲間たち Advent Calendar 2015 24日目、クリスマスイブですね。一昨日(12/22)にEngineer All Handsという社内のイベントでLTをすることになり、「LDAPと私」というネタで ansible-ldap というモジュールの話を軽くしました。ついでにブログにでもちゃんと書いておこうかなと思い、昨日アドベントカレンダーの予定を見てみたら、空いていたので参加してみました。

概要

この記事の要点としては以下のとおりです。これを読んで理解できる方はその後を読む必要はありません。モジュールのソースコード内のドキュメントを読みましょう。

  • ansible-ldapの ldap_entry および ldap_attr モジュールだけで LDAPのエントリーの追加・削除、エントリーの属性の追加、削除、置換を冪等に行うことができる
  • ldapi:// にも対応しているので slapd-config(5)の設定もできる
  • Ansible Galaxy には登録されてないのでパッチ書いてPR送った (mergeされるかは別の話)

あと、LDAPには直接関係ありませんが、

ansible-ldapとは

django-otpdjango-auth-ldapmockldap などの作者の Peter Sagerson 氏が開発された Ansib用の LDAP モジュールです。django-auth-ldapやmockldapには前職でとてもお世話になってました。 [1]

ldap_entryldap_attr の2つのモジュールがあります。 前者はLDAPのDITに対し、entryを追加または削除を、後者はすでに存在するエントリーの属性を追加、削除、もしくは置き換えを行うためのモジュールです。

fumiyasuさん が一日目にslapd.conf(5)を使ったPlaybookの 記事 を書かれていますが、ansible-ldapモジュールを使う場合は、slapd-config(5)を使います。DebianやUbuntuのOpenLDAPサーバのパッケージである、slapdパッケージはslapd-config(5)がデフォルトです。slapd-config(5)絡みの記事は 以前書いたものある ので、そちらもご参照ください。

使い方

これらのモジュールを使った サンプルのPlaybook を作ったのでそれを例にして説明します。

ldap_entry

まずはシンプルに memberof モジュールを追加する例です。

- name: be sure memberof module
  ldap_entry:
    dn: cn=module{1},cn=config
    state: present
    objectClass: olcModuleList
    olcModulePath: /usr/lib/ldap
    olcModuleLoad: memberof.la
dn
必須項目です。エントリーの追加時、 {1} とあるindex番号は必要に応じて自動的に付加されます。しかし、 cn=module,cn=configcn=module{1},cn=config は別のdnになります。そのためエントリー追加後、同様にindexを指定しないで属性の変更や、エントリーの削除を行おうとすると、エントリーが見つからないためエラーになります。変更するエントリーのindexを把握しておく必要があるのでサンプルで明示的に指定しています。
state
デフォルトは present で存在しなければ作成し、存在すれば何もしません。削除するときは absent を使います。これはAnsibleの他モジュールと同じ挙動です。 ldap_entry には変更という操作はありません。つまり ldapadd コマンドと ldapdelete コマンドに相当する操作だけです。 ldapmodify コマンドに相当する操作は ldap_attr で行います。
objectClass 及びその他の属性
必要な場合は設定します。 slapd-config(5)の属性の名称はslapd.conf(5) の設定オプション名とは微妙に異なるので、man slapd-config(5)GLOBAL CONFIGURATION OPTIONS 以降のセクションを参照しましょう。

次に、suffixが dc=example,dc=org のLDAPディレクトリに対し、oganizational unitを追加する例を見てみます。

- ldap_entry:
    dn: "ou={{ item }},{{ suffix }}"
    objectClass: organizationalUnit
    ou: "{{ item }}"
    bind_dn: "cn=admin,{{ suffix }}"
    bind_pw: "{{ admin_pw }}"
    state: present
  with_items:
    - People
    - Groups
    - SUDOers
server_uri
この例では省略していますが、これはデフォルトでは ldapi:/// になります。リモートホストのLDAPサーバを対象にする場合には、LDAPのURLを指定する必要があります。
start_tls
StartTLS LDAPを使うときはこのオプションを true にします。デフォルトでは false です。
bind_dnbind_pw
slapd-config(5) の設定の時は省略することで EXTERNAL mechanism でアクセスしますが、特定のDITの操作を行うには、デフォルトでは認証が必要になります。bind用のDNを bind_dn で、パスワードを bind_pw で指定します。slapd自体の設定し、特定のsuffix のDITに対し変更を行う場合、つい忘れがちなので気をつけましょう。

ldap_attr

アクセス権限を設定する例を見てみます。

- name: olcAccess are absent.
  ldap_attr:
     dn: "olcDatabase={1}{{ backend | lower }},cn=config"
     name: olcAccess
     state: absent
     values:
       - '{0}to attrs=userPassword by self write by anonymous auth by * none'
       - '{1}to attrs=shadowLastChange by self write by * read'
       - '{2}to * by * read'

- name: olcAccess are present.
   ldap_attr:
     dn: "olcDatabase={1}{{ backend | lower }},cn=config"
     name: olcAccess
     state: present
     values:
       - '{0}to attrs=userPassword,shadowLastChange
          by self write
          by anonymous auth
          by dn="cn=admin,{{ suffix }}" write
          by * none'
       - '{1}to dn.base=""
          by * read'
       - '{2}to *
          by dn="cn=admin,{{ suffix }}" write
          by * read'
       - '{3}to dn.subtree="{{ suffix }}"
          by self read
          by * read'
       - '{4}to *
          by * none'

この2つのタスクでは、 absent でslapdインストール時にデフォルトで設定されるアクセス設定を一度削除し、 present で新しく設定しています。

このやり方は面倒ですね。代わりに exact を使えばひとつのタスクで変更できます。

- name: override olcAccess exactly
  ldap_attr:
    dn: "olcDatabase={1}{{ backend | lower }},cn=config"
    name: olcAccess
    state: exact
    values:
      - '{0}to attrs=userPassword,shadowLastChange
         by self write
         by anonymous auth
         by dn="cn=admin,{{ suffix }}" write
         by * none'
      - '{1}to dn.base=""
         by * read'
      - '{2}to *
         by dn="cn=admin,{{ suffix }}" write
         by * read'
      - '{3}to dn.subtree="{{ suffix }}"
         by self read
         by * read'
      - '{4}to *
         by * none'

name で 変更する属性を指定し、 values で一つもしくは一つ以上の値を指定します。複数設定できるか否かは、設定するattributeのスキーマ次第です。他のパラメータは基本的には ldap_entry と同じです。

Note

backend には MDB を指定しています。MDBは LMDB をバックエンドとするためのdebconfのパラメータです。DNでは小文字になるため、lowerフィルターを使って小文字に変換しています。

ansible-ldapのインストール方法

現状、ansible-ldapは Ansible Galaxyには登録されてません。また、Ansible Galaxyで公開できる形式になっていないため、requirements.yml に

- src: https://bitbucket.org/psagers/ansible-ldap
  name: ldap
  scm: hg

のように記述し、 ansible-galaxy install -p library -r requirements.yml と実行してもインストールできません。手動で hg clone を実行し、playbookのlibraryディレクトリを以下にモジュールをコピーする必要があります。とても面倒です。ということで、パッチ書いてPRを送っておきました。

マージされるまでの間 [2] は、下記のように記述することで ansible-galaxy install コマンドでインストールすることができます。ただし、 --no-deps オプションが必要ですので気をつけましょう。

- src: https://bitbucket.org/mkouhei/ansible-ldap
  name: ldap
  scm: hg
  version: for-ansible-galaxy

さらにもしAnsible Galaxyに登録されたら、おそらくこんな記述になることでしょう。

- src: psagers.ldap

C bindingとPure Python

今回紹介した django-auth-ldap、mock-ldapは OpenLDAPライブラリの C bindingと実装された Python-LDAP やそのPython3対応としてのforkの pyldap に依存してします。 ansible-ldapもPython-LDAPに依存しています。 [3] Pure PythonでのLDAPクライアントの実装としての ldap3 は使用されていません。今回紹介する ansible-ldap も やはり Python-LDAPに依存しています。

今までに何度かLDAP用のAnsibleモジュールを書こうかな、と思ったことも何度かあったのですが [4] 、slapdの設定変更に必要なのは ldapi:// (LDAP over IPC) でアクセス、操作できることなので、少なくともこの10月末まではC bindingのPython-LDAP / pyldapしかその機能があるPythonモジュールはありませんでした。 Pure Pythonのldap3では本当にこの最近(2015-11-15)、 v0.9.9.3 として LDAPIの機能が実装された ようです。

一方、このansible-ldapは 昨年の11月に基本機能を実装して公開されていた いたので、ldap3を使っていないのは当然といえます。

DebianシステムではPython-LDAPは python-ldap パッケージとして提供されていますが、pyldapはDebianパッケージとして提供されていません。ldap3 は python-ldap3 (Python2版) および python3-ldap3 (Python3版) として提供されています。Ansible は Python3はまだ正式対応されていないので現状では playbook の中で、

- apt: pkg=python-ldap state=present

python-ldap パッケージをインストールすればよいですが [5] 、Ubuntu の次のLTSではPython3だけになるので、pyenvなどでPython2.7を構築するタスクを書いた上 [6]

- apt:
    pkg={{ item }}
    state=present
  with_items:
    - build-essential
    - libldap2-dev
  - pip: name=Python-LDAP

として、slapdを動かすホスト上でPython-LDAPのコンパイルも必要な上、ansible-ldapでは現状任意の PYTHONPATH を指定することができないので、 /usr/local/lib/python2.7/dist-packages の下にPython-LDAPをインストール必要があります。

(おまけ) CSVからvarsを読み込むモジュールを作りました

サンプルのPlaybookではユーザーの作成や、SSH公開鍵を登録するための タスク もあるのですが、ユーザー作成用のパラメータや公開鍵をいちいちYAMLで記述するのはとても億劫です。なので、CSVで記述したものをvarsとして読み込むことのできる include_csv というモジュールもついでに作りました。使い方としては、コアモジュールの include_vars のような使い方になります。詳しくはAnsible GalaxyのREADMEのページを参照してください。

まとめ

今までは、Ansibleらしくない書き方でslapdの構築を行い、それが故に冪等にすることが難しいため変更はAnsibleで行わない、という運用になってしまっていましたが、このansible-ldapモジュールのおかげで冪等性を保つことができるようになりました。個人的には ldapvi コマンド、Python-LDAPに続く、LDAPの運用・利用が非常に楽になるツールが登場したと思ってます。勝手に三種の神器と呼びたい。

また、include_csv も便利そうという意見ももらったので結構うれしいですね。 [7]

footnotes

[1]11月からRuby on Railsの仕事をすることになり、業務では現時点ではPythonもLDAPも使っていません。
[2]マージされるかはわかりませんが。
[3]ansible 2.0.0-0.8.rc3 も試してみましたが、現状ではまだ ansible-galaxy コマンドが Python3 に対応していませんでした。
[4]頻度の問題で、結局作らずに済ませてしまってきたのですが…。
[5]Debianシステムの場合。
[6]今回の話と少しずれるので省略します。
[7]ちなみに今回、初Ansible Galaxy、つまり初のAnsible モジュール作成、初のアカウント作成、初のRole登録、初の ansible-galaxy コマンド利用、と初ものづくしでした。