概要
よくあるケースとしてはダイナミックにサーバをプロビジョニングしたい場合かなと思います
簡単な playbook を作成した後でプログラマブルに Ansible を実行してみます
環境
- macOS 10.13.5
- Python 2.7.15
- Ansible 2.5.5
事前準備
今回は Mac で行っています
pip からインストールしました
Homebrew でもインストールできるのでそれでも問題ないです
簡単な playbook の作成
非常に簡単なやつを作成します
ターゲットのサーバに対してファイルを touch するだけです
全体は以下の通りです
.
├── production
├── roles
│ └── file
│ └── tasks
│ ├── main.yml
│ └── new.yml
└── site.yml
[ubuntu]
172.28.128.3
この Ubuntu サーバは vagrant を使ってローカルに立てています
サーバは何でも OK なので適当に用意してください
- vim roles/file/tasks/main.yml
- include: new.yml
- vim roles/file/tasks/new.yml
- file:
path: /tmp/hoge
state: touch
main.yml は参照するだけで本体は new.yml を作成してそっちに書いています
Ansible 側の内容はこれだけです
実行は
ansible-playbook -k -i production site.yml
です
-k
オプションは SSH のパスワードを対話的に指定するためのオプションです
これで問題なく動作すれば playbook の準備は OK です
Tips
少し自分がはまった点を紹介します
fatal: [172.28.128.3]: FAILED! => {"changed": false, "module_stderr": "", "module_stdout": "/bin/sh: 1: /usr/bin/python: n
ot found\r\n", "msg": "MODULE FAILURE", "rc": 127}
で実行できない状態がありました
ターゲットのマシンである Ubuntu に Python をインストールしてあげることで解決しました
Vagrant の xenial を使っていると Python がデフォルトでインストールされていないようです
Ansible は基本的に 2.7 以上の Python が /usr/bin/python
にインストールされている想定だそうです
https://docs.ansible.com/ansible/2.4/faq.html
一応 Python なしでも実行する方法はあるらしいのですが基本はいるようです
Ansible を Python からキックするスクリプトの作成
ここからが本番です
先ほど作成した playbook と同じことを Python から実行してみます
結構長いのであとで詳細を説明します
改行部分がまとまりとなっています
import json
import shutil
from collections import namedtuple
from ansible.parsing.dataloader import DataLoader
from ansible.vars.manager import VariableManager
from ansible.inventory.manager import InventoryManager
from ansible.playbook.play import Play
from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.plugins.callback import CallbackBase
import ansible.constants as C
class ResultCallback(CallbackBase):
def v2_runner_on_ok(self, result, **kwargs):
host = result._host
print(json.dumps({host.name: result._result}, indent=4))
def v2_runner_on_failed(self, result, *args, **kwargs):
host = result._host
print(json.dumps({host.name: result._result}, indent=4))
def v2_runner_on_unreachable(self, result):
host = result._host
print(json.dumps({host.name: result._result}, indent=4))
Options = namedtuple('Options', ['connection', 'module_path', 'forks', 'become', 'become_method', 'become_user', 'check', 'diff', 'remote_user'])
options = Options(connection='paramiko', module_path=['/usr/share/ansible'], forks=10, become=None, become_method=None, become_user=None, check=False, diff=False, remote_user='root')
loader = DataLoader()
results_callback = ResultCallback()
host_list = ['./production']
sources = ','.join(host_list)
inventory = InventoryManager(loader=loader, sources=host_list)
variable_manager = VariableManager(loader=loader, inventory=inventory)
play_source = dict(
name = "Ansible Play",
hosts = ['ubuntu'],
gather_facts = 'no',
tasks = [
dict(action=dict(module='file', args=dict(path='/tmp/hoge', state='touch')))
]
)
play = Play().load(play_source, variable_manager=variable_manager, loader=loader)
tqm = None
passwords = dict()
try:
tqm = TaskQueueManager(
inventory=inventory,
variable_manager=variable_manager,
loader=loader,
options=options,
passwords=passwords,
stdout_callback=results_callback,
)
result = tqm.run(play)
finally:
if tqm is not None:
tqm.cleanup()
shutil.rmtree(C.DEFAULT_LOCAL_TMP, True)
まず全体的な流れとして
- 必要なライブラリの読み込み -> コールバック処理の定義 -> オプションの設定 -> インベントリ情報の生成 -> playbook の定義 -> 実行
という感じになります
それぞれ順を追って説明します
詳細説明
まず import 系ですがこれはほぼすべて必須かなと思います
import json
import shutil
from collections import namedtuple
from ansible.parsing.dataloader import DataLoader
from ansible.vars.manager import VariableManager
from ansible.inventory.manager import InventoryManager
from ansible.playbook.play import Play
from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.plugins.callback import CallbackBase
import ansible.constants as C
各流れの中で使用するものになります
次にコールバック用のクラスを先に作成します
class ResultCallback(CallbackBase):
def v2_runner_on_ok(self, result, **kwargs):
host = result._host
print(json.dumps({host.name: result._result}, indent=4))
def v2_runner_on_failed(self, result, *args, **kwargs):
host = result._host
print(json.dumps({host.name: result._result}, indent=4))
def v2_runner_on_unreachable(self, result):
host = result._host
print(json.dumps({host.name: result._result}, indent=4))
results_callback = ResultCallback()
コールバッククラスは何に使うかというと単純に playbook が終了した際に何をするか定義することができます
今回は「成功」「失敗」「アクセスできない」の 3 つのイベントをハンドリングして内容を表示しているだけです
ここで定義したコールバッククラスはあとで使用します
次にオプションの設定です
これは CLI を実行する際のオプションと同じですが Python からコールする際には必須のオプションがあります
Options = namedtuple('Options', ['connection', 'module_path', 'forks', 'become', 'become_method', 'become_user', 'check', 'diff', 'remote_user'])
options = Options(connection='paramiko', module_path=['/usr/share/ansible'], forks=10, become=None, become_method=None, become_user=None, check=False, diff=False, remote_user='root')
最後の remote_user
だけ追加しました
それ以外は公式で紹介していた部分になるので必須かなと思います
例えば ansible-playbook コマンドでオプションを指定する必要がある場合はここで追加します
今回の場合であれば -k
と -i
ですがこれらは InventroyManager で定義することになります (ややこしい)
次に InventoryManager になります
loader = DataLoader()
host_list = ['./production']
sources = ','.join(host_list)
inventory = InventoryManager(loader=loader, sources=host_list)
variable_manager = VariableManager(loader=loader, inventory=inventory)
ポイントは定義したインベントリファイル (production
) を読み込んでいる部分です
実はファイルを使わないでも直接ここでインベントリ情報を定義することができます
が実はファイルを使ったほうが理由があります
それは SSH 時のパスワードを定義するためです
先ほどの production から実は以下のように変更しています
[ubuntu]
172.28.128.3
[ubuntu:vars]
ansible_ssh_user='root'
ansible_ssh_pass='xxxxxxxxxx'
Python から実行する際に -k
を使って対話的に実行することはできません
なので、パスワード情報を事前にインベントリファイルに定義しておく必要があります
このパスワード情報を InventoryManager
に食わせる方法がファイルからしかやり方がわからなかったため、既存のインベントリファイルを使うようにしています
次に playbook の定義です
play_source = dict(
name = "Ansible Play",
hosts = ['ubuntu'],
gather_facts = 'no',
tasks = [
dict(action=dict(module='file', args=dict(path='/tmp/hoge', state='touch')))
]
)
play = Play().load(play_source, variable_manager=variable_manager, loader=loader)
ポイントは hosts と tasks になります
hosts ではインベントリファイルに書かれているホスト情報のうちどれを実行するかを配列で定義できます
今回は 1 台だけなのでインベントリファイルに書かれているホストを 1 台定義しています
tasks ですが、ここにメインとなるプロビジョニング処理を定義する必要があります
tasks は先ほどのインベントリファイルとは違いファイルを流用することができません (今回の方法だとないだけで一応ファイルから実行する方法もあるっぽいです PlaybookExecutor
?)
なので、ここで同じ内容を定義し直す必要があります
今回であれば file
モジュールを使って新規でファイルを touch しているだけなのでそこまでコードにするのは難しくありません
あとは Play().load()
でこれまでに定義した情報を食わせれば playbook が出来上がります
最後に実行部分です
tqm = None
passwords = dict()
try:
tqm = TaskQueueManager(
inventory=inventory,
variable_manager=variable_manager,
loader=loader,
options=options,
passwords=passwords,
stdout_callback=results_callback,
)
result = tqm.run(play)
finally:
if tqm is not None:
tqm.cleanup()
shutil.rmtree(C.DEFAULT_LOCAL_TMP, True)
実行には TaskQueueManager
を使います
これを使うことで実行結果をコールバックとして設定することができます
基本は定義してものを食わせていけば OK です
あとは run()
するだけです
動作確認
と実行すれば OK です
結果は以下のような感じで JSON で表示されます
{
"172.28.128.3": {
"_ansible_parsed": true,
"group": "root",
"uid": 0,
"dest": "/tmp/hoge",
"changed": true,
"state": "file",
"gid": 0,
"mode": "0644",
"invocation": {
"module_args": {
"directory_mode": null,
"force": false,
"remote_src": null,
"path": "/tmp/hoge",
"owner": null,
"follow": true,
"group": null,
"unsafe_writes": null,
"state": "touch",
"content": null,
"serole": null,
"diff_peek": null,
"setype": null,
"selevel": null,
"original_basename": null,
"regexp": null,
"validate": null,
"src": null,
"seuser": null,
"recurse": false,
"delimiter": null,
"mode": null,
"attributes": null,
"backup": null
}
},
"owner": "root",
"diff": {
"after": {
"path": "/tmp/hoge",
"state": "touch"
},
"before": {
"path": "/tmp/hoge",
"state": "absent"
}
},
"size": 0,
"_ansible_no_log": false
}
}
ターゲットのサーバに入ってファイルができているかも確認すると良いかもしれません
一応これで Python から同じ内容の playbook を実行することはできました
最後に
ansible-playbook を Python から実行してみました
正直辛い感じを覚えました
今回のやり方は公式に書いてあるサンプルを元にしているので、おそらく王道だと思います
一番辛いのは情報が少ないことです
タスクの定義の仕方などは各モジュールごとにいろいろと違うと思いますが、それをコードに落とす方法は Try&Error を繰り返すしかないかと思います
これをやりたいケースとしては既存の playbook をプログラミングから実行したい場合が一番多いかなと思います
その場合に今回の方法だと既存の playbook が使い回せないのが辛い点かなと思います
playbook を作る際にはじめからこの方法を取るのであれば何とかなるかもしれません
ただ今回紹介した構成はかなりミニマムな構成なのでここから group_vars
やタスクの templates
などが絡んでくると思うので、その場合にどうするかも調査しないとダメだと思います
他にやり方がないかもう少し見てみようと思います
参考サイト