Semaphore での Consul 動的インベントリ
概要
このガイドでは、HashiCorp Consul を Semaphore の動的インベントリソースとして使用する方法を説明します。ホストを手動で列挙する代わりに、Ansible が実行時に Consul のカタログに問い合わせて対象ホストを検出します。
この方法では、Git リポジトリにコミットした Python インベントリスクリプトを使用します。Semaphore は playbook の実行時にこのスクリプトを自動的に実行します。
前提条件
- ノードが登録済みの稼働中の Consul クラスター
- カタログへの読み取りアクセス権を持つ Consul ACL トークン (ACL が有効な場合のみ)
- Semaphore ホスト (または runner) にインストールされた Python 3
- playbook とインベントリスクリプトを保存する Git リポジトリ
ステップ 1 — インベントリスクリプトの作成
リポジトリに inventory/consul_inventory.py というファイルを作成します。このスクリプトは Consul の HTTP API に問い合わせ、Ansible が期待する形式でホスト情報を返します。
#!/usr/bin/env python3
"""
Consul dynamic inventory for Ansible.
Groups nodes by node_meta values and filters out unhealthy nodes.
"""
import json
import os
import sys
import urllib.request
import ssl
CONSUL_ADDR = os.environ.get("CONSUL_HTTP_ADDR", "https://consul.example.com")
CONSUL_TOKEN = os.environ.get("CONSUL_HTTP_TOKEN", "")
def consul_get(path):
url = f"{CONSUL_ADDR}/v1/{path}"
req = urllib.request.Request(url)
if CONSUL_TOKEN:
req.add_header("X-Consul-Token", CONSUL_TOKEN)
ctx = ssl.create_default_context()
with urllib.request.urlopen(req, context=ctx) as resp:
return json.loads(resp.read())
def is_healthy(node_name):
"""Return True if the node has a passing serfHealth check."""
try:
checks = consul_get(f"health/node/{node_name}")
return any(
c["CheckID"] == "serfHealth" and c["Status"] == "passing"
for c in checks
)
except Exception:
return False
def build_inventory():
inventory = {"_meta": {"hostvars": {}}}
all_hosts = []
for node in consul_get("catalog/nodes"):
name = node["Node"]
if not is_healthy(name):
continue
all_hosts.append(name)
inventory["_meta"]["hostvars"][name] = {
"ansible_host": node["Address"],
"ansible_user": "your_ssh_user",
"ansible_python_interpreter": "/usr/bin/python3",
}
inventory["all"] = {"hosts": all_hosts}
return inventory
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "--host":
print(json.dumps({}))
else:
print(json.dumps(build_inventory(), indent=2))
スクリプトに実行権限を付与します。
chmod +x inventory/consul_inventory.py
このスクリプトは、Consul のノードメタデータ、サービスタグ、データセンターごとにホストをグループ化するようにカスタマイズできます。上記の例は最小限の出発点です。
ステップ 2 — リポジトリの準備
リポジトリは次のような構成になります。
my-project/
├── inventory/
│ └── consul_inventory.py
└── playbook.yml
この方法では、Python の標準ライブラリのみを使用して Consul API に直接問い合わせます。インベントリスクリプトを動作させるために追加の Ansible コレクションは必要ありません。
簡単なテスト用 playbook (playbook.yml):
- hosts: all
gather_facts: false
tasks:
- name: Hello World
ansible.builtin.debug:
msg: "Hello from {{ inventory_hostname }}"
このリポジトリを Git プロバイダーにプッシュします。
ステップ 3 — Semaphore の設定
変数グループの追加
インベントリスクリプトは、Consul のアドレスとトークンを環境変数から読み取ります。これらの値を渡すために、Semaphore で変数グループを作成します。
- プロジェクトに移動し、変数グループをクリックします
- 新しい変数グループをクリックします
- 名前を付けます (例:
consul-inventory) - 環境変数に次を追加します:
CONSUL_HTTP_ADDR=https://consul.example.comCONSUL_HTTP_TOKEN=your-consul-acl-token(Consul クラスターで ACL が有効な場合のみ必要)
- 作成をクリックします
Consul クラスターで ACL が有効になっていない場合は、CONSUL_HTTP_TOKEN 変数を省略できます。インベントリスクリプトはそのまま動作し、API リクエストに認証トークンを送信しないだけです。
リポジトリの追加
- リポ ジトリに移動し、新しいリポジトリをクリックします
- リポジトリの Git URL を入力します
- Git プロバイダー用のアクセスキーを選択します
- 作成をクリックします
インベントリの追加
- インベントリに移動し、新しいインベントリをクリックします
- 名前を付けます (例:
consul-dynamic-inventory) - 種類としてファイルを選択します
- パス
inventory/consul_inventory.pyを入力します - Ansible がホストへの接続に使用する SSH キーを選択します
- 作成をクリックします
パスは Git リポジトリのルートからの相対パスです。Semaphore はリポジトリをクローンし、このパスを ansible-playbook -i inventory/consul_inventory.py に渡します。
タスクテンプレートの作成
- タスクテンプレートに移動し、新しいテンプレートをクリックします
- 名前を付けます (例:
Consul Hello World) - Playbook に
playbook.ymlを設定します - 上で作成したリポジトリ、インベントリ、変数グループを選択します
- 作成をクリックします
ステップ 4 — 実行
タスクテンプレートで実行をクリックします。Semaphore は次の処理を行います。
- リポジトリをクローンする
- Consul インベントリスクリプトを使用して playbook を実行する
- タスクログに出力を表示する
次のような出力が表示されるはずです。
TASK [Hello World] *************************************************************
ok: [node-01] => {
"msg": "Hello from node-01"
}
ok: [node-02] => {
"msg": "Hello from node-02"
}
メタデータによるホストのグループ化
Consul はノードメタデータ (各ノードに付与されるキーと値のペア) をサポートしています。これを利用して Ansible のグループを自動的に作成できます。
スクリプトの build_inventory() 関数内で、ホスト変数を設定した後に次のコードを追加します。
# Get node metadata
node_detail = consul_get(f"catalog/node/{name}")
meta = node_detail.get("Node", {}).get("Meta", {})
# Group by metadata keys
for key in ("role", "env", "os"):
val = meta.get(key)
if val:
group = f"{key}_{val}"
inventory.setdefault(group, {"hosts": []})
inventory[group]["hosts"].append(name)
これにより、role_webserver、env_production、os_ubuntu のようなグ ループが作成されます。playbook でこれらを対象に指定できます。
- hosts: role_webserver
tasks:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted