ようやく自宅サーバや開発環境仮想マシンの構築手順を Ansible 化した。

この手のツールはハマりポイント+よく使うイディオムを抑えるのが大事。Pythonプロダクトらしく非常にドキュメントが充実しているのだけど、充実しすぎているのでポイントを自分のためにもまとめておくことにする。

例はおもに Ansible のドキュメントから引用させていただきました。

設定ファイルを書きかえる(一行)

lineinfile モジュールを使う。使い方は以下のような形になる。

name: enalbe sudo without password if user belongs to the wheel group
lineinfile: "dest=/etc/sudoers state=present regexp='^%wheel' line='%wheel ALL=(ALL) NOPASSWD: ALL'"

stateabsent にすれば削除することができる。動きとしては regexp にマッチする行をみつけたら line に書きかえる、という動作になる。またデフォルトでは regexp にマッチする行がない場合最終行に追加される。

上記のほかにも regexp にマッチする業の前後に追記するなど柔軟な動作が可能。

ファイルコピーを行う(再帰的)

copy モジュールを使えばファイルコピーできるのは当たり前なのだが、じゃあディレクトリの場合どうするかというとてっとりばやくは以下のようにするとよい。

shell: rsync -a /path_to/source/  /path_to/dest/ creates=/path_to/dest/hoge

このとおり、 rsync してしまうのがよい。上記はターゲットマシン上どうしでの rsync で、playbookを実行しているホスト→ターゲットマシンで実行する場合は以下。

local_action: command rsync -a /path_to/source/ {{ inventory_hostname }}:/path_to/dest/

local_action を使うとターゲット上でなくplaybookを実行しているホスト上でコマンドを実行できる。

make installする

make install するソフトウェアをどうするか。一つ目のパターンはよく見る形で以下のように1個ずつタスクにする。

- name: "wget hoge src"
  command: wget -O http://example.com/hoge.tar.gz  creates=hoge.tar.gz

- name: "expand src"
  command: tar xvfz hoge.tar.gz creates=hoge

# 続く…

私の場合こっちというのは以下のような形で1個にまとめてしまう。たかが make install に上記のようにつらつらタスクを書きまくるのはしんどいので。

name: install python3.3
shell: >-
  wget http://www.python.org/ftp/python/3.3.2/Python-3.3.2.tgz &&
  tar zxvf Python-3.3.2.tgz &&
  rm -f Python-3.3.2.tgz &&
  cd Python-3.3.2 &&
  ./configure --prefix=/usr/local/python3.3.2 --enable-shared &&
  make &&
  paco -D make install
  chdir=/usr/local/src creates=/usr/local/python3.3.2/bin/python3.3

もちろん、途中でこけると中途半端なことになるのだがそうそうコケないのでこれでよいと思っている。

変数を使いこなす

ターゲット固有の情報はデフォルトで収集される。内容を見たければ ansible hostname -m setup と実行すればよい。

他のホストの情報は以下のようにすればアクセスできる。

{{ hostvars['test.example.com']['ansible_distribution'] }}

変数は実行時にコマンドラインオプションで上書きできる。

ansible-playbook release.yml --extra-vars "version=1.23.45 other_variable=foo"

ループを使いこなす

Ansibleは結構複雑なループが使える。ただ実用的な範囲で言うと以下ぐらいを抑えておくとよいのでは。

with_items による単純ループ

name: add several users
user: name={{ item }} state=present groups=wheel
with_items:
   - testuser1
   - testuser2

with_items + 辞書によるループ

name: add several users
user: name={{ item.name }} state=present groups={{ item.groups }}
with_items:
  - { name: 'testuser1', groups: 'wheel' }
  - { name: 'testuser2', groups: 'root' }

コマンド実行結果を with_items で回す

- name: retrieve the list of home directories
  command: ls /home
  register: home_dirs

- name: add home dirs to the backup spooler
  file: path=/mnt/bkspool/{{ item }} src=/home/{{ item }} state=link
  with_items: home_dirs.stdout_lines
  # same as with_items: home_dirs.stdout.split()

with_sequence による整数範囲ループ(Pythonの range のようなもの)

file: dest=/var/stuff/{{ item }} state=directory
with_sequence: start=4 end=16 stride=2

do-until(+sleep)ループ

action: shell /usr/bin/foo
register: result
until: result.stdout.find("all systems go") != -1
retries: 5
delay: 10

ファイルglobでループ

copy: src={{ item }} dest=/etc/fooapp/ owner=root mode=600
with_fileglob:
  - /playbooks/files/fooapp/*

条件分岐を使いこなす

これまた条件分岐もいろいろできることはあるのだが、以下のパターンを抑えておくとよいと思う。

単純な変数による分岐

- name: "shutdown Debian flavored systems"
  command: /sbin/shutdown -t now
  when: ansible_os_family == "Debian"

コマンド実行結果による分岐

tasks:
  # まずはコマンドを実行してresultに格納
  - shell: /usr/bin/foo
    register: result
    ignore_errors: True

  # タスクが失敗した場合
  - debug: msg="it failed"
    when: result|failed

  # タスクにより更新された場合
  - debug: msg="it changed"
    when: result|changed

  # タスクが成功した場合
  - debug: msg="it succeeded"
    when: result|success

  # タスクがskipされた場合
  - debug: msg="it was skipped"
    when: result|skipped

  # タスクの標準出力によって分岐
  - shell: echo "hi does not found"
    when: result.stdout.find('hi') != -1

複数ターゲットが関連するタスクを実行する

ローリングアップデートのように複数ターゲットが連動して動くタスクがある。その場合、playbookを実行するマシンを核としてタスクを作ればよい。

- hosts: webservers
  serial: 5

  tasks:
    - name: take out of load balancer pool
      local_action: command /usr/bin/take_out_of_pool {{ inventory_hostname }}

    - name: actual steps would go here
      yum: name=acme-web-stack state=latest

    - name: add back to load balancer pool
      local_action: command: /usr/bin/add_back_to_pool {{ inventory_hostname }}

local_action モジュールはplaybookを実行しているホストで実行するコマンドを定義する。上記の例だとWEBサーバを5並列でLB切り離し→更新→LB組み込みを実行する。


この程度を理解していればだいたいやりたいことはできるはず。あとはモジュール一覧を頭にいれるだけですね。ついつい shell でゴリ押ししそうになるのでぐっとそこをこらえて…


Python3も3.3になり性能も向上して、移行が順調に進んできているように見える。それを考慮して最近Python3系と2系の互換性確保を見直した。

ひとことでいうと「Python3中心」にした。

今までの手法

基本的に3系と2系の互換性確保は「単一ソースコード」でやっていてそれは今も変わらず。

で、今までは Python3とPython2の中間 のコードを書いていた。組み込みの関数のリネームなどはしない、というような(例えば zipizip で上書いてしまうとか)。ただこの手法だといざPython2系のサポートをdropする場合に処理的にもコード的にも無駄が多くなる。

今の手法

rayscompat.py では以下のような方針とした。

  • Python3中心。
    • Python3の時に効率的に動作する
    • Python2のサポートドロップ時にソース修正が容易

なのでガンガンPython2の関数はPython3に置き換える。( mapimap にしてしまうなど ) 大まかには compat.py のコメント通り以下のようなコーディング。

  • 文字列は strbytes を使う。
  • range , map , zip などはイテレータを返すものとする。
  • コレクションの列挙は iter_items などの関数を通す。
  • iterator.next() の代わりに next 関数を使う。
  • クラスは object の代わりに compatobject を継承する。

    • __str__ , __nonzero__ , __cmp__ など廃止・仕様変更されたメソッドは Python3の仕様で実装する。 compatobject が Python2の場合は自動で変換する。
  • リネームされたモジュールのimportは compat_import メソッドを使う。

    • 例: compat_import(py2="email.MIMEMultipart", py3="email.mime.multipart")
      • このようにするとPython2でも email.mime.multipart でアクセスできる。
  • urllib は複雑にリネームされているので、 python2用 urllib を作ってしまう。

    • Python2でも普通に import urllib.request のように書ける

キモは compat_import かなと。

そうすると、以下のような感じでほぼPython3のみ対象としたのと同じソースコードになる。Python2サポートdrop時は単純なソースコード置換でいけるはず。

import urllib.request
compat_import(py2="cookielib", py3="http.cookiejar")

class Html(compatobject):
  def __init__(self, text):
    if isinstance(text, bytes):
      text = text.decode("utf8")
    self.text = text

  def __str__(self):
    return self.text

cookie_jar = http.cookiejar.MozillaCookieJar()
handlers = [urllib.request.HTTPCookieProcessor(cookie_jar)]
opener = urllib.request.build_opener(*handlers)
html = Html(opener.open("http://www.google.co.jp/").read())

ソースコードの寿命をのばすためにもボチボチこの方針で書き変えていくつもり。


放置して久しいこのブログ、やっと重い腰をあげていろいろ手をいれた。

  • HTML5化
  • ブログシステムを入れ替え

silkylog

このブログはxreaのレンタルサーバで動かしているのだけど、しばらく見ないうちにxreaもサーバがすごく増えて、新しいサーバではpython2.7.5が使えるようになっていた。

なので自作のwebフレームワーク rays を使った新しいブログシステム silkylog に移行した。silkylogはpython3.3でも動くのでしばらくは安心。これまで使っていたのはpython2系でしか動かないし、かといってpython3対応させる気力も起きないしでどーしたものかと思っていた。

流行に乗ってOctpressとかにしようかなあ、と思いつつせっかく作ったのだしと思って。ただなんとなく悔しかったのでsilkylogにOctpressと同等の完全に静的なサイトを出力できる機能も付けた。

さらにこれまでのDBベースからファイルベースに変更した。Blosxomみたいにファイルを直接検索してもいいんだけどタギングを考慮するとそれはそれでめんどくさいので、インデックス情報のみsqliteで保持するような中途半端なカタチに。

手前味噌ながらraysの機能を余すところなくつかって、シンプルに作れたと思う。

html5化(デザイン)

ようやくhtml5に。デザインも見直し。シンプルでよいデザインにできた・・・と思う。cssはscssでまとめた。 レスポンシブルデザインにも対応。コメントはDisqus、シンタックスハイライトはgoogle code prettifyと外部だよりに。

マークアップはreStructuredTextに統一した。普及度を考えるとmarkdownなのだけどやはりmarkdownは表現力が低くて。いろんな方言が乱立してるし、独自に拡張をするしくみに乏しいので使いづらかった。

ここのところ、Pythonかどうかに関わらずドキュメントはsphinxで書いているので、日常的に書いているのはreStructuredTextが圧倒的だし、ディレクティブが簡単に作れるのもよい。


ともかく、これでしばらくは手をいれなくてすむ・・・はず。