構成管理ツール Ansibleを2週間使ってみて

この投稿は自分のブログ記事をQiita用に整形したものです。追記事項があればブログの方に書いていきます。
[構成管理ツール Ansibleを試す (FFmpegのPlaybookを作る)](http://rest-term.com/archives/3106/)

![ansiblelogo.png](https://qiita-image-store.s3.amazonaws.com/0/3215/e4906cae-9a43-c559-2b47-ebe7e8ebf903.png)

今や構成管理ツールは百花繚乱、何を使ったらいいのかよくわからないのが正直なところなのですが、以前から気になっていた[Ansible](http://www.ansibleworks.com/)をちょっと触ってみました。

> Ansible is a radically simple IT orchestration engine that makes your applications and systems easier to deploy. Avoid writing scripts or custom code to deploy and update your applications— automate in a language that approaches plain English, using SSH, with no agents to install on remote systems.

選んだ理由としては、ミドルウェアが増えない、サーバ/クライアントに特別なエージェント(デーモン)が不要という2点です。

ソフトウェアのインストールなどサーバ構築の手順は Playbook と呼ばれるYAMLファイルに記述します。スクリプト言語(DSL含む)と比較すると表現力は劣りますが、手軽に書けるところは魅力です。より複雑な処理が必要な場合はモジュール化してYAMLから呼び出してねっていうスタンスみたいです。

作業にあたって、公式リファレンス以外では以下のページを参考にさせていただきました。日本語で良くまとまっているので理解が進みました。

* [Ansible Tutorial](http://yteraoka.github.io/ansible-tutorial/)
* [Ansible in detail](http://yteraoka.github.io/ansible-tutorial/ansible-in-detail.html)
* [ansibleを使ってみる](http://tdoc.info/blog/2013/04/20/ansible.html)

## 目次

* Ansibleのインストール
* FFmpegをインストールするPlaybook
* サポートされているCPU命令に応じて処理を変える
* ゲートウェイサーバを経由させる
* 感想など

### 環境

* CentOS 6.4 (x86_64/Intel Xeon E5-2690 2.90GHz 仮想32コア/96GB RAM)
* Ansible 1.4
* GCC 4.4.7
* Python 2.7.5
* リモートサーバ CentOS 6.4 (x86_64) 50台 (東日本DCに10台、西日本DCに40台)

リモートサーバ環境は、OSが同じでCPUなどのハードウェア構成はそれぞれ異なっているサーバ群を対象に検証しました。

## Ansibleのインストール
Ansible本体は yum ではなく pip でインストールしました。依存モジュールとして paramiko/PyYAML/Jinja2 もいっしょにインストールされます。Ansible本体を動作させるにはPython 2.6以降が必要ですが、CentOS 6.x系であればシステムのPythonでその条件を満たしています。

```bash
$ sudo pip install ansible
```

## FFmpegをインストールするPlaybook
映像配信サービス向けのエンコードサーバクラスタを構築するという想定で、[FFmpeg](http://www.ffmpeg.org/)をサーバにインストールするPlaybookを作ってみます。FFmpegは任意のメディアコーデック/コンテナライブラリを組み込んでビルドする大きなソフトウェアです。今回は以下の構成でFFmpegをビルド/インストールします。rpm/debパッケージで配布されているものはコーデック/コンテナ不足 & 最適化不足のため実務では使えないかと思います。

1. Gitのインストール
1. CMakeのインストール
1. Yasm (x86アセンブラ)のソースコードのダウンロード
1. Yasm をコンパイル
1. Yasm のインストール
1. x264 (H.264/AVCビデオコーデック)のソースコードのダウンロード
1. x264 のコンパイル
1. x264 のインストール
1. libfdk-aac (AACオーディオコーデック)のソースコードのダウンロード
1. libfdk-aac のコンパイル
1. libfdk-aac のインストール
1. libmp3lame (MP3オーディオコーデック)のソースコードのダウンロード
1. libmp3lame のコンパイル
1. libmp3lame のインストール
1. libogg (ビットストリームライブラリ)のソースコードのダウンロード
1. libogg のコンパイル
1. libogg のインストール
1. libopus (Opusオーディオコーデック)のソースコードのダウンロード
1. libopus のコンパイル
1. libopus のインストール
1. libvorbis (Vorbisオーディオコーデック)のソースコードのダウンロード
1. libvorbis のコンパイル
1. libvorbis のインストール
1. libvpx (VP8/VP9ビデオコーデック)のソースコードのダウンロード
1. libvpx のコンパイル
1. libvpx のインストール
1. FFmpeg のソースコードのダウンロード
1. FFmpeg のコンパイル
1. FFmpeg のインストール

libvpx には[VP9コーデック](http://en.wikipedia.org/wiki/VP9)が含まれているのでぜひ加えておきましょう。libtheora (Theoraコーデック) はこれまで組み込んでも実際は使う機会がほとんどなかったので外しました。あとは、ネットラジオのアーカイブとか作るような人なら libopus も入れておきましょう。[Opus](http://ja.wikipedia.org/wiki/Opus_(%E9%9F%B3%E5%A3%B0%E5%9C%A7%E7%B8%AE))は今後普及が期待されるオーディオコーデックです。WebRTCのMTIコーデック(対応必須ですよという意味、ちなみにビデオコーデックはまだ決まっていない)にもなっているので、HTML5を勉強している人も知っておく必要があるかと思います。

また、上記の構成でビルドされたバイナリは libfdk-aac のライセンスがGPL非互換のため **再配布不可** なので注意してください。別のAACコーデックである libfaac を組み込んだ場合も同様です。ソフトウェアライセンス関連はまだ単純でいいのですが、ビジネスだとパテント絡みで利権やくざが怖いです。

参考：
2003年の記事ですけどこれはひどいですね。資本主義とはいったい。。
[MPEG-4 backers protest Microsoft license](http://news.cnet.com/2100-1023-980007.html)
(要約：MPEG LAがMicrosoftに対して 「Windows Mediaのライセンスは安すぎる！不正競争だ！」)

話が逸れました。
まず、デプロイ対象ホストを Inventory host file(インベントリファイル) と呼ばれるINIファイルに書いておきます。

**servers.ini**

```ini
[servers]
enc[01:50].broadcast.yahoo.co.jp ansible_ssh_port=2222
```

インベントリファイルはYAMLではないんですね。なぜでしょう。

まずは疎通確認してみます。

```bash
## ansible コマンドで全てのホストに疎通確認(pingモジュール利用)
## Options:
##   -i: specify inventory host file
##   -m: module name to execute
$ ansible all -i servers.ini -m ping
enc01.broadcast.yahoo.co.jp | success >> {
    "changed": false,
    "ping": "pong"
}

enc02.broadcast.yahoo.co.jp | success >> {
    "changed": false,
    "ping": "pong"
}
... 以下続く
```

FFmpegの Playbook は以下のように作りました。さすがに150行を超えていると分割したくなりますけど、FFmpegの場合はコーデック/コンテナライブラリ毎に分割するとファイル数が増えて管理が面倒になる気がします。どれくらいの粒度で分割すれば良いかは調整していく必要がありそうです。

Playbookでは以下の標準モジュールを利用しています (モジュール一覧: [Ansible Modules](http://www.ansibleworks.com/docs/modules.html))

* command
* get_url
* git
* shell
* yum

**ffmpeg.yml**

```yaml
- hosts: servers
  user: ryo
  sudo: no
  vars:
    prefix: /usr/local/ffmpeg
    ffmpeg_version: 2.1.1
    libmp3lame_version: 3.99
    libmp3lame_patch_version: 5
    libogg_version: 1.3.1
    libopus_version: 1.0.3
    libvorbis_version: 1.3.3
    make_jobs: "{{ansible_processor_count * ansible_processor_cores}}"
  tasks:
    - name: install git
      sudo: yes
      yum: pkg=git.x86_64 state=installed
    - name: install cmake28
      sudo: yes
      yum: pkg=cmake28.x86_64 state=installed
    - name: download yasm source code
      git: repo=git://github.com/yasm/yasm.git dest=/tmp/yasm
    - name: compile yasm
      shell: >-
        cmake28 -DCMAKE_INSTALL_PREFIX={{prefix}} . &&
        make -j{{make_jobs}}
        chdir=/tmp/yasm
        creates=yasm
    - name: install yasm
      sudo: yes
      shell: >-
        make install &&
        ldconfig {{prefix}}/lib
        chdir=/tmp/yasm
        creates={{prefix}}/lib/libyasm.so
    - name: download x264 source code
      git: repo=git://git.videolan.org/x264.git dest=/tmp/x264
    - name: compile x264
      shell: >-
        PATH={{prefix}}/bin:$PATH &&
        ./configure --prefix={{prefix}} --enable-shared --enable-pic --extra-cflags="-march=native -mfpmath=sse -msse2" &&
        make -j{{make_jobs}}
        chdir=/tmp/x264
        creates=x264
    - name: install x264
      sudo: yes
      shell: >-
        make install &&
        ldconfig {{prefix}}/lib
        chdir=/tmp/x264
        creates={{prefix}}/bin/x264
    - name: download libfdk_aac source code
      git: repo=git://git.code.sf.net/p/opencore-amr/fdk-aac dest=/tmp/libfdk_aac
    - name: compile libfdk_aac
      shell: >-
        ./autogen.sh &&
        ./configure --prefix={{prefix}} --enable-shared &&
        make -j{{make_jobs}}
        chdir=/tmp/libfdk_aac
        creates=libfdk-aac.la
    - name: install libfdk_aac
      sudo: yes
      shell: >-
        make install &&
        ldconfig {{prefix}}/lib
        chdir=/tmp/libfdk_aac
        creates={{prefix}}/lib/libfdk-aac.so
    - name: download libmp3lame source code
      get_url: url=http://sourceforge.net/projects/lame/files/lame/{{libmp3lame_version}}/lame-{{libmp3lame_version}}.{{libmp3lame_patch_version}}.tar.gz/download dest=/tmp/lame.tar.gz
    - name: extract libmp3lame gzip archive
      command: tar zxf lame.tar.gz chdir=/tmp creates=lame-{{libmp3lame_version}}.{{libmp3lame_patch_version}}
    - name: compile libmp3lame
      shell: >-
        ./configure --prefix={{prefix}} &&
        make -j{{make_jobs}}
        chdir=/tmp/lame-{{libmp3lame_version}}.{{libmp3lame_patch_version}}
        creates=libmp3lame/libmp3lame.la
    - name: install libmp3lame
      sudo: yes
      shell: >-
        make install &&
        ldconfig {{prefix}}/lib
        chdir=/tmp/lame-{{libmp3lame_version}}.{{libmp3lame_patch_version}}
        creates={{prefix}}/lib/libmp3lame.so
    - name: download libogg source code
      get_url: url=http://downloads.xiph.org/releases/ogg/libogg-{{libogg_version}}.tar.gz dest=/tmp/libogg.tar.gz
    - name: extract libogg gzip archive
      command: tar zxf libogg.tar.gz chdir=/tmp creates=libogg-{{libogg_version}}
    - name: compile libogg
      shell: >-
        ./configure --prefix={{prefix}} &&
        make -j{{make_jobs}}
        chdir=/tmp/libogg-{{libogg_version}}
        creates=src/libogg.la
    - name: install libogg
      sudo: yes
      shell: >-
        make install &&
        ldconfig {{prefix}}/lib
        chdir=/tmp/libogg-{{libogg_version}}
        creates={{prefix}}/lib/libogg.so
    - name: download libopus source code
      get_url: url=http://downloads.xiph.org/releases/opus/opus-{{libopus_version}}.tar.gz dest=/tmp/libopus.tar.gz
    - name: extract libopus gzip archive
      command: tar zxf libopus.tar.gz chdir=/tmp creates=opus-{{libopus_version}}
    - name: compile libopus
      shell: >-
        ./configure --prefix={{prefix}} &&
        make -j{{make_jobs}}
        chdir=/tmp/opus-{{libopus_version}}
        creates=libopus.la
    - name: install libopus
      sudo: yes
      shell: >-
        make install &&
        ldconfig {{prefix}}/lib
        chdir=/tmp/opus-{{libopus_version}}
        creates={{prefix}}/lib/libopus.so
    - name: download libvorbis source code
      get_url: url=http://downloads.xiph.org/releases/vorbis/libvorbis-{{libvorbis_version}}.tar.gz dest=/tmp/libvorbis.tar.gz
    - name: extract libvorbis gzip archive
      command: tar zxf libvorbis.tar.gz chdir=/tmp creates=libvorbis-{{libvorbis_version}}
    - name: compile libvorbis
      shell: >-
        ./configure --prefix={{prefix}} &&
        make -j{{make_jobs}}
        chdir=/tmp/libvorbis-{{libvorbis_version}}
        creates=lib/libvorbis.la
    - name: install libvorbis
      sudo: yes
      shell: >-
        make install &&
        ldconfig {{prefix}}/lib
        chdir=/tmp/libvorbis-{{libvorbis_version}}
        creates={{prefix}}/lib/libvorbis.so
    - name: download libvpx source code
      git: repo=http://git.chromium.org/webm/libvpx.git dest=/tmp/libvpx
    - name: compile libvpx
      shell: >-
        PATH={{prefix}}/bin:$PATH &&
        ./configure --prefix={{prefix}} --enable-vp9 --enable-shared --enable-pic --disable-examples &&
        make -j{{make_jobs}}
        chdir=/tmp/libvpx
        creates=libvpx.so
    - name: install libvpx
      sudo: yes
      shell: >-
        make install &&
        ldconfig {{prefix}}/lib
        chdir=/tmp/libvpx
        creates={{prefix}}/lib/libvpx.so
    - name: download ffmpeg source code
      get_url: url=http://www.ffmpeg.org/releases/ffmpeg-{{ffmpeg_version}}.tar.bz2 dest=/tmp/ffmpeg.tar.bz2
    - name: extract ffmpeg bzip2 archive
      command: tar jxf ffmpeg.tar.bz2 chdir=/tmp creates=ffmpeg-{{ffmpeg_version}}
    - name: compile ffmpeg
      shell: >-
        PATH={{prefix}}/bin:$PATH &&
        PKG_CONFIG_PATH={{prefix}}/lib/pkgconfig:$PKG_CONFIG_PATH &&
        ./configure --prefix={{prefix}} --cc=gcc --extra-ldflags='-L{{prefix}}/lib' --extra-cflags='-I{{prefix}}/include -march=native -mfpmath=sse -msse2' --optflags='-O2 -ffast-math -fomit-frame-pointer -finline-functions' --disable-avisynth --enable-avfilter --enable-libfdk-aac --enable-libmp3lame --enable-libopus --enable-libvorbis --enable-libvpx --enable-libx264 --enable-postproc --enable-pthreads --enable-shared --enable-swscale --enable-vdpau --enable-x11grab --enable-gpl --enable-nonfree --enable-version3 &&
        make -j{{make_jobs}}
        chdir=/tmp/ffmpeg-{{ffmpeg_version}}
        creates=ffmpeg
    - name: install ffmpeg
      sudo: yes
      shell: >-
        make install &&
        ldconfig {{prefix}}/lib
        chdir=/tmp/ffmpeg-{{ffmpeg_version}}
        creates={{prefix}}/bin/ffmpeg
```

**実行**

```bash
## ansible-playbook コマンドでFFmpegをインストール
## Options:
##   -i: specify inventory host file
##   -f: specify number of parallel processes to use 
##   -k: ask for SSH password
##   -K: ask for sudo password
$ ansible-playbook -i servers.ini ffmpeg.yml -f 10 -k -K
SSH password:
sudo password [defaults to SSH password]:
PLAY [servers] ****************************************************************

GATHERING FACTS ***************************************************************
ok: [enc01.broadcast.yahoo.co.jp]
ok: [enc02.broadcast.yahoo.co.jp]
ok: [enc03.broadcast.yahoo.co.jp]
... 省略
ok: [enc50.broadcast.yahoo.co.jp]

TASK: [install git] ***********************************************************
ok: [enc01.broadcast.yahoo.co.jp]
... 省略

TASK: [install ffmpeg] ********************************************************
changed: [enc01.broadcast.yahoo.co.jp]
... 省略

PLAY RECAP ********************************************************************
enc01.broadcast.yahoo.co.jp    : ok=28   changed=16   unreachable=0    failed=0
enc02.broadcast.yahoo.co.jp    : ok=25   changed=19   unreachable=0    failed=0
enc03.broadcast.yahoo.co.jp    : ok=30   changed=14   unreachable=0    failed=0
... 省略
enc50.broadcast.yahoo.co.jp    : ok=30   changed=14   unreachable=0    failed=0
```

実際に50台のサーバを対象に実行してみると普通に何度か失敗しました。ネットワークや筐体障害というのは日常的にあることですので、リモートホストが生きていれば時間を置いて再度実行することになります。ただし今回の例では全てのソフトウェアをバージョン指定でインストールしているわけではないので、べき等性(何度実行しても状態は変わらない)は保証されません。

映像配信サービス用にエンコードサーバクラスタを構築することを考えた時に、全ホストのOSは統一はできてもCPUなどのハードウェア環境はそうもいかないことが多いと思います。よって、各環境に最適化されたバイナリをデプロイするには現地でコンパイルする必要があります。

## サポートされているCPU命令に応じて処理を変える
リモートサーバのCPU/コア数、サポートされている命令を判別してコンパイル時に適切な CFLAGS を設定できるようにしたいのですけど、なにかスマートな方法はないものでしょうか。

リモートサーバのシステム情報(Ansibleの文脈では **FACTS** と呼ばれる)について確認するには [setup モジュール](http://www.ansibleworks.com/docs/modules.html#setup)を利用します。

```bash
$ ansible -m setup enc01.broadcast.yahoo.co.jp
enc01.broadcast.yahoo.co.jp | success >> {
    "ansible_facts": {
    ... 省略
    "ansible_processor": [
        "Intel(R) Xeon(R) CPU E5-2690 0 @ 2.90GHz",
        "Intel(R) Xeon(R) CPU E5-2690 0 @ 2.90GHz",
        "Intel(R) Xeon(R) CPU E5-2690 0 @ 2.90GHz",
        ... 以下、仮想コアの数だけずらずら並ぶ
    ],
    "ansible_processor_cores": 8,
    "ansible_processor_count": 2,
    "ansible_processor_threads_per_core": 2,
    "ansible_processor_vcpus": 32
    ... 省略
```

ansible_processor はプロセッサ名の文字列が仮想コアの数だけ詰まってるリストになっています。上記例では物理CPU (ansible_processor_count)が2つ、物理CPU内のコア(ansible_processor_cores)が8つ、各コアがHTの2スレッドで仮想32コアの環境になります。コンパイル時のcc1の並列実行数には ansible_processor_count * ansible_processor_cores の値を指定するのが効率良さそうです。スラッシングを避けるためにも並列数はあまり増やさない方がいいと思います。

```yaml
## 上記FFmpegインストール用 Playbookの場合は以下のように修正
make_jobs: "{{ansible_processor_count * ansible_processor_cores}}"
```

次に、サポートされているCPU命令に応じて処理を分ける場合です。こちらはAnsibleの setup モジュールだと flags の値は取れないようですので、setup モジュールを修正するか新しいモジュールを作る他なさそうです。一応 [ohai モジュール](http://www.ansibleworks.com/docs/modules.html#ohai)を使えばできそうなんですが、Ruby処理系はともかく mixlib-* などたくさんの追加依存モジュールによって環境が汚れるのはツライところです。

setup モジュールには Hardware クラスというのがあって、LinuxHardware や SunOSHardware などOS毎にサブクラスが定義されています。Linux OSを対象とする場合は LinuxHardware クラスの get_cpu_facts メソッドをいじります。/proc/cpuinfo の内容をバラしてるだけの処理ですので修正は簡単です。

setup モジュールに以下のパッチを当てると、サポートされているCPU命令のリストとL2キャッシュサイズ(KB)がFACTSに追加されます。

* [setup.patch](http://rest-term.com/wp-content/uploads/2013/12/setup.patch) (Ansible 1.4で動作確認済)

```yaml
## 追加されるFACTSの一例 (ただしdistcc環境は考慮していない)
"ansible_processor_cache_size": 20480,
"ansible_processor_flags": [
            "fpu",
            "de",
            "pse",
            "tsc",
            "msr",
            "pae",
            "mce",
            "cx8",
            "apic",
            "mtrr",
            "pge",
            "mca",
            "cmov",
            "pat",
            "pse36",
            "clflush",
            "mmx",
            "fxsr",
            "sse",
            "sse2",
            "ht",
            "syscall",
            "nx",
            "lm",
            "constant_tsc",
            "arch_perfmon",
            "unfair_spinlock",
            "pni",
            "pclmulqdq",
            "ssse3",
            "cx16",
            "sse4_1",
            "sse4_2",
            "x2apic",
            "popcnt",
            "aes",
            "xsave",
            "avx",
            "hypervisor",
            "xsaveopt"
        ],
```

コンパイラがGCCの場合、-march=native がサポートされていないプラットフォーム上でソフトウェアをコンパイルする際は上記情報を使って CFLAGS を組み立てます。

setup モジュールを修正するのではなく新規にモジュールを作る場合、出力結果のJSONに ansible_facts というキーを追加して、そこに任意の情報を入れておけばAnsibleがFACTSに追加してくれるようです。サポートされているCPU命令のリストやL2キャッシュサイズ、コンパイラのバージョン(gccなら -dumpversion の値)を集めて、最終的に Autotools や CMake に渡す CFLAGS (あるいはCXXFLAGS)を組み立てるロジックを埋め込んでおくと便利そうです。

* [Module Provided ‘Facts’](http://www.ansibleworks.com/docs/developing_modules.html#id8)

ここではマルチメディア処理や機械学習処理など、CPUバウンドな科学技術計算を扱うソフトウェアをインストールする場合に考えなければならない問題を取り上げてみました。

## ゲートウェイサーバを経由させる
[Ansible Advent Calender 2013](http://qiita.com/advent-calendar/2013/ansible)に良いエントリーがありました。MSP屋さんの視点は貴重ですね。

* [あるインフラエンジニアとAnsibleの付き合い方](http://memo.laughk.org/2013/12/10/ansible_with_infrastructure_engineer.html)

ネットワークセグメントが異なる自社DC間の通信において、ゲートウェイサーバ(踏み台)経由でないとSSHセッションが貼れないケースが考えられます(コンプライアンスの対象範囲を狭めたり、セキュリティ上の理由から)。ゲートウェイサーバではソフトウェアの新規インストールが許可されておらず、なおかつ実行可能なコマンドも厳しく制限されていることが多いため、これを解決できないとAnsibleの導入は難しくなります。

そこでAnsibleではSSHに渡すオプションを **ANSIBLE_SSH_ARGS** 環境変数で指定できるようなのでこれを利用します。ここから先はAnsible自体とは関係ない話なので詳細は上記エントリー等を参照ください。ssh_config で ProxyCommand を使ってゲートウェイサーバを経由させるだけです。

ただし、ゲートウェイサーバのOpenSSHがv5.4未満かつ nc コマンドが入っていなくて新規インストールもできない環境だとお手上げですかね？ あと、最近流行ってるワンタイムキーによる二段階認証が必要な場合も厳しいかなぁと思います。

## 感想など
Ansibleは導入コストの低さも魅力ですが、ミドルウェアが増えない点が特に良いです。

Playbook内でいっぱい変数を使ったりRole分割しすぎると訳が分からなくなってくるので、上手くバランスを取って整備する必要があると感じました。今回作成したFFmpegインストール用のPlaybookの場合は、コーデック/コンテナライブラリ毎に分割すると細かくなりすぎて整備が煩雑になる気がするので、例えば映像関連と音声関連とで分けるくらいの粒度なら整備しやすいかもしれません。

ところで、Slashdotで他のツールとの比較記事がありました。

* [PuppetとChef、Ansible、Saltの比較レビュー、Puppetが高評価](http://it.slashdot.jp/story/13/11/26/0728236/)

言われてみればWeb UIについては考えていませんでした。確かにWebブラウザから構成管理情報が閲覧できるとシステム運用者は嬉しいと思います。

以前は映像配信サービスの開発/運用に携わっていましたが、配信系だけでも数百台規模でしたし、エンコーダやストレージサーバクラスタも全て構成管理するのはちょっと面倒でした。ソフトウェアも商用ライセンスでたくさん買ってたので、インストールされているサーバ台数は正確に計上しなければなりません。そういった面でノード情報をINIファイルで管理するAnsibleでは正直心配なので、結局は構成管理用サーバを別に立ち上げてデータベースにノード情報と併せて管理することになるかもしれません。それやったらChefでいいやろって話になりますけども。

使い始めてまだ2週間弱ですけど、覚えることが少なくて使いやすいなと思いました。でもきっと年明けにはぜんぶ忘れていることでしょう。