Ansible Callback Plugin ile Çalışma Çıktısını Özelleştirme

Ansible çalıştırdığında terminale dökülen o renk renk, satır satır çıktıyı hepimiz tanırız. Kimi zaman bu çıktı tam olarak ihtiyacınıza cevap verir, kimi zaman ise “neden bu kadar gürültü var?” ya da “neden bu kadar az bilgi var?” diye düşünürsünüz. İşte tam bu noktada Callback Plugin kavramı devreye giriyor. Ansible’ın çalışma çıktısını baştan sona şekillendirebileceğiniz, log formatını değiştirebileceğiniz, hatta çıktıyı bir veritabanına ya da Slack kanalına gönderebileceğiniz bu mekanizma, ciddi bir otomasyon altyapısı kuruyorsanız mutlaka öğrenmeniz gereken bir konu.

Callback Plugin Nedir ve Neden Önemlidir?

Ansible, her task’ı çalıştırdığında iç yapısında çeşitli olaylar tetiklenir. Bir task başladığında, bittiğinde, başarısız olduğunda ya da playbook tamamlandığında bu olaylar fırlar. Callback Plugin, tam olarak bu olayları yakalayan ve bunlara tepki veren bileşenlerdir.

Varsayılan olarak Ansible, default adlı callback plugin’i kullanır. Bu plugin terminale renk kodlarıyla birlikte standart çıktıyı yazar. Ama siz:

  • Çıktıyı JSON formatında bir dosyaya kaydetmek istiyorsanız
  • Her task’ın süresini ölçmek ve yavaş olanları tespit etmek istiyorsanız
  • Bir playbook başarısız olduğunda Slack’e otomatik bildirim göndermek istiyorsanız
  • Kurumsal log yönetim sisteminize (ELK, Splunk gibi) veri aktarmak istiyorsanız

…o zaman callback plugin yazmak tam size göre.

Callback Plugin Türleri

Ansible’da callback plugin’ler üç farklı modda çalışabilir:

  • stdout: Terminale yazılan ana çıktıyı tamamen değiştirip yönetir. Aynı anda yalnızca bir tane aktif olabilir.
  • notification: Yan etkiler için kullanılır, ana çıktıyı değiştirmez. Birden fazla aktif olabilir.
  • aggregate: Tüm çalışma bittiğinde özet bilgi üretmek için kullanılır.

Pratikte en çok stdout tipini kullanırsınız ama notification tipi de özellikle alert/bildirim senaryolarında çok işe yarar.

Mevcut Callback Plugin’leri Keşfetmek

Kendi plugin’inizi yazmadan önce Ansible’ın zaten hazır sunduğu plugin’lere bir bakın. Pek çok ihtiyacınız zaten karşılanmış olabilir.

ansible-doc -t callback -l

Bu komut mevcut tüm callback plugin’lerini listeler. Sık kullanılanlardan bazıları:

  • default: Standart renkli terminal çıktısı
  • json: Tüm çıktıyı JSON formatında verir
  • yaml: Çıktıyı YAML benzeri daha okunaklı formatta gösterir
  • timer: Playbook’un toplam süresini gösterir
  • profile_tasks: Her task’ın ne kadar sürdüğünü gösterir
  • mail: Hata durumunda e-posta gönderir
  • slack: Slack kanalına bildirim gönderir

Bir plugin hakkında detaylı bilgi almak için:

ansible-doc -t callback profile_tasks

Hazır Plugin’leri Aktif Etmek

Bir callback plugin’i aktif etmenin en kolay yolu ansible.cfg dosyasını düzenlemektir.

# ansible.cfg
[defaults]
# Stdout plugin'ini değiştirmek için
stdout_callback = yaml

# Ek notification plugin'leri eklemek için (virgülle ayırın)
callback_whitelist = profile_tasks, timer, mail

Ansible 2.11 ve sonrasında callback_whitelist yerine callbacks_enabled kullanılmaktadır:

# ansible.cfg (yeni stil)
[defaults]
stdout_callback = yaml
callbacks_enabled = profile_tasks, timer

yaml callback’ini aktif ettiğinizde çıktı çok daha okunaklı hale gelir. Özellikle büyük debug çıktılarında farkı anında görürsünüz.

Kendi Callback Plugin’inizi Yazmak

İşte asıl eğlenceli kısım burası. Kendi ihtiyacınıza özel bir plugin yazmak düşündüğünüzden çok daha kolay.

Proje Yapısını Hazırlamak

Ansible, callback plugin’leri birkaç farklı konumda arar:

  • Proje dizininde: ./callback_plugins/
  • Role içinde: roles/role_adi/callback_plugins/
  • Ansible konfigürasyonunda belirtilen yolda: callback_plugins_path
  • Sistem genelinde: /usr/share/ansible/plugins/callback/

En pratik yöntem proje dizininde callback_plugins klasörü oluşturmaktır:

mkdir -p ~/ansible-projem/callback_plugins
cd ~/ansible-projem

Basit Bir Callback Plugin Örneği

Önce en temel haliyle bir plugin yazalım. Bu plugin, her task tamamlandığında bir log dosyasına yazar:

# callback_plugins/simple_logger.py

from ansible.plugins.callback import CallbackBase
from datetime import datetime

DOCUMENTATION = '''
    callback: simple_logger
    type: notification
    short_description: Task sonuçlarini log dosyasina yazar
    description:
      - Her task tamamlandiginda sonucu /var/log/ansible_tasks.log dosyasina yazar
'''

class CallbackModule(CallbackBase):
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'notification'
    CALLBACK_NAME = 'simple_logger'
    CALLBACK_NEEDS_ENABLED = True

    def __init__(self):
        super(CallbackModule, self).__init__()
        self.log_file = '/tmp/ansible_tasks.log'

    def _log(self, message):
        timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        with open(self.log_file, 'a') as f:
            f.write(f'[{timestamp}] {message}n')

    def v2_runner_on_ok(self, result):
        host = result._host.get_name()
        task = result._task.get_name()
        self._log(f'OK | Host: {host} | Task: {task}')

    def v2_runner_on_failed(self, result, ignore_errors=False):
        host = result._host.get_name()
        task = result._task.get_name()
        msg = result._result.get('msg', 'Bilinmeyen hata')
        self._log(f'FAILED | Host: {host} | Task: {task} | Hata: {msg}')

    def v2_runner_on_skipped(self, result):
        host = result._host.get_name()
        task = result._task.get_name()
        self._log(f'SKIPPED | Host: {host} | Task: {task}')

    def v2_playbook_on_stats(self, stats):
        hosts = sorted(stats.processed.keys())
        for host in hosts:
            s = stats.summarize(host)
            self._log(
                f'STATS | Host: {host} | ok={s["ok"]} '
                f'changed={s["changed"]} failed={s["failures"]} '
                f'skipped={s["skipped"]}'
            )

Slack Bildirimi Gönderen Plugin

Gerçek dünya senaryolarında en çok ihtiyaç duyulan şeylerden biri: bir playbook başarısız olduğunda anında haber almak. Şimdi bunu Slack üzerinden yapalım:

# callback_plugins/slack_alert.py

import json
import urllib.request
import urllib.error
from ansible.plugins.callback import CallbackBase

DOCUMENTATION = '''
    callback: slack_alert
    type: notification
    short_description: Basarisizlik durumunda Slack bildirimi gonder
'''

class CallbackModule(CallbackBase):
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'notification'
    CALLBACK_NAME = 'slack_alert'
    CALLBACK_NEEDS_ENABLED = True

    def __init__(self):
        super(CallbackModule, self).__init__()
        # Bunu ortam degiskeninden ya da ansible.cfg'den alabilirsiniz
        self.webhook_url = 'https://hooks.slack.com/services/XXXXX/XXXXX/XXXXX'
        self.failed_tasks = []
        self.playbook_name = ''

    def v2_playbook_on_start(self, playbook):
        self.playbook_name = playbook._file_name

    def v2_runner_on_failed(self, result, ignore_errors=False):
        if not ignore_errors:
            host = result._host.get_name()
            task = result._task.get_name()
            msg = result._result.get('msg', 'Detay yok')
            self.failed_tasks.append({
                'host': host,
                'task': task,
                'msg': msg
            })

    def v2_playbook_on_stats(self, stats):
        if not self.failed_tasks:
            return

        # Basarisizlik varsa Slack'e bildir
        blocks = [
            {
                "type": "header",
                "text": {
                    "type": "plain_text",
                    "text": f":red_circle: Ansible Hatasi: {self.playbook_name}"
                }
            }
        ]

        for ft in self.failed_tasks:
            blocks.append({
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": (
                        f"*Host:* {ft['host']}n"
                        f"*Task:* {ft['task']}n"
                        f"*Hata:* {ft['msg']}"
                    )
                }
            })

        payload = json.dumps({"blocks": blocks}).encode('utf-8')
        req = urllib.request.Request(
            self.webhook_url,
            data=payload,
            headers={'Content-Type': 'application/json'}
        )

        try:
            urllib.request.urlopen(req)
        except urllib.error.URLError as e:
            self._display.warning(f'Slack bildirimi gonderilemedi: {e}')

Performans Takip Plugin’i

Bir diğer çok kullanışlı senaryo: hangi task’lar ne kadar sürüyor? Production ortamında playbook’larınız yavaşlamaya başladığında bunu tespit etmek için şu plugin’i kullanabilirsiniz:

# callback_plugins/perf_tracker.py

import time
from collections import OrderedDict
from ansible.plugins.callback import CallbackBase

DOCUMENTATION = '''
    callback: perf_tracker
    type: aggregate
    short_description: Task surelerini olcer ve rapor olusturur
'''

class CallbackModule(CallbackBase):
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'aggregate'
    CALLBACK_NAME = 'perf_tracker'
    CALLBACK_NEEDS_ENABLED = True

    def __init__(self):
        super(CallbackModule, self).__init__()
        self.task_times = OrderedDict()
        self.current_task = None
        self.task_start_time = None
        self.slow_threshold = 10  # 10 saniyenin uzerindekiler yavas sayilir

    def v2_playbook_on_task_start(self, task, is_conditional):
        self.current_task = task.get_name()
        self.task_start_time = time.time()

    def _record_task(self, result):
        if self.current_task and self.task_start_time:
            elapsed = time.time() - self.task_start_time
            task_key = f"{self.current_task} [{result._host.get_name()}]"
            self.task_times[task_key] = elapsed

    def v2_runner_on_ok(self, result):
        self._record_task(result)

    def v2_runner_on_failed(self, result, ignore_errors=False):
        self._record_task(result)

    def v2_runner_on_skipped(self, result):
        self._record_task(result)

    def v2_playbook_on_stats(self, stats):
        self._display.banner('PERFORMANS RAPORU')

        if not self.task_times:
            self._display.display('Hicbir task suresi kaydedilemedi.')
            return

        # Sureye gore sirala (uzundan kisaya)
        sorted_tasks = sorted(
            self.task_times.items(),
            key=lambda x: x[1],
            reverse=True
        )

        total_time = sum(self.task_times.values())
        slow_tasks = [(t, d) for t, d in sorted_tasks if d >= self.slow_threshold]

        self._display.display(f'Toplam sure: {total_time:.2f} saniye')
        self._display.display(f'Task sayisi: {len(self.task_times)}')
        self._display.display('')

        if slow_tasks:
            self._display.display(
                f'YAVAS TASKLAR ({self.slow_threshold}s uzerinde):',
                color='yellow'
            )
            for task_name, duration in slow_tasks:
                self._display.display(
                    f'  {duration:6.2f}s  {task_name}',
                    color='red'
                )
            self._display.display('')

        self._display.display('EN UZUN SURE ALAN 5 TASK:')
        for task_name, duration in sorted_tasks[:5]:
            bar = '#' * int(duration / total_time * 20)
            self._display.display(f'  {duration:6.2f}s [{bar:<20}] {task_name}')

Plugin’i ansible.cfg’de Aktif Etmek

Yazdığınız plugin’leri aktif etmek için:

# ansible.cfg
[defaults]
stdout_callback = yaml
callbacks_enabled = simple_logger, slack_alert, perf_tracker

# Plugin klasoru varsayilan konumdan farkliysa
callback_plugins = ./callback_plugins:/usr/share/ansible/plugins/callback

CALLBACK_NEEDS_ENABLED = True ayarladıysanız plugin’in callbacks_enabled listesinde olması gerekir. False yaptığınızda otomatik yüklenir ama bu genellikle önerilmez çünkü istem dışı yan etkiler yaratabilir.

Ortam Değişkenleri ile Plugin Konfigürasyonu

Plugin’inize dışarıdan parametre geçmenin en temiz yolu ansible.cfg üzerinden konfigürasyon okumaktır. Slack webhook URL’ini hardcode etmek yerine şöyle yapabilirsiniz:

# callback_plugins/slack_alert_v2.py - Konfigurasyonlu versiyon

import os
from ansible.plugins.callback import CallbackBase
from ansible import constants as C

class CallbackModule(CallbackBase):
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'notification'
    CALLBACK_NAME = 'slack_alert_v2'
    CALLBACK_NEEDS_ENABLED = True

    def set_options(self, task_keys=None, var_options=None, direct=None):
        super(CallbackModule, self).set_options(
            task_keys=task_keys,
            var_options=var_options,
            direct=direct
        )
        # Ortam degiskeninden ya da ansible.cfg'den oku
        self.webhook_url = os.environ.get(
            'ANSIBLE_SLACK_WEBHOOK',
            self.get_option('webhook_url') if self.has_option('webhook_url') else ''
        )
        self.channel = os.environ.get('ANSIBLE_SLACK_CHANNEL', '#ops-alerts')

    def v2_runner_on_failed(self, result, ignore_errors=False):
        if not self.webhook_url:
            self._display.warning(
                'slack_alert_v2: ANSIBLE_SLACK_WEBHOOK tanimlanmamis, bildirim atlanıyor.'
            )
            return
        # ... bildirim gonderme kodu

Çalıştırırken:

export ANSIBLE_SLACK_WEBHOOK="https://hooks.slack.com/services/XXXXX"
ansible-playbook site.yml -i inventory/production

Gerçek Dünya Senaryosu: CI/CD Pipeline Entegrasyonu

GitLab CI ya da Jenkins ile entegre bir Ansible altyapısı kuruyorsanız, çıktıları yapılandırılmış formatta almanız kritik önem taşır. JSON callback tam bu iş için biçilmiş kaftan:

# .gitlab-ci.yml veya Makefile icin
ANSIBLE_STDOUT_CALLBACK=json ansible-playbook 
  -i inventory/staging 
  deploy.yml 2>&1 | tee /tmp/ansible_output.json

# Ciktiyi parse edip basarisiz taskları bul
python3 -c "
import json, sys

with open('/tmp/ansible_output.json') as f:
    data = json.load(f)

plays = data.get('plays', [])
failed = []

for play in plays:
    for task in play.get('tasks', []):
        for host, result in task.get('hosts', {}).items():
            if result.get('failed', False):
                failed.append({
                    'host': host,
                    'task': task['task']['name'],
                    'msg': result.get('msg', '')
                })

if failed:
    print('BASARISIZ TASKLAR:')
    for f in failed:
        print(f'  [{f["host"]}] {f["task"]}: {f["msg"]}')
    sys.exit(1)
else:
    print('Tum tasklar basarili!')
"

Plugin Geliştirirken Dikkat Edilmesi Gerekenler

Birkaç önemli pratik noktayı paylaşmak istiyorum, bunları kendim acı deneyimlerle öğrendim:

  • Thread safety: Ansible bazı modları paralel çalıştırır. Dosyaya yazarken threading.Lock() kullanmayı ihmal etmeyin, yoksa log dosyanız karışabilir.
  • Exception handling: Plugin’inizdeki her hata Ansible çalışmasını durdurabiliyor. try/except bloklarını esirgemeyin.
  • CALLBACK_NEEDS_ENABLED: Güvenli tarafta kalmak için bunu her zaman True yapın.
  • self._display kullanımı: Plugin içinden terminale mesaj basmak için print() değil, her zaman self._display.display() kullanın. Aksi halde çıktı sıralaması bozulabilir.
  • Versiyon uyumluluğu: Ansible 2.8 öncesi callback API’si farklıydı. CALLBACK_VERSION = 2.0 ile yeni API’yi kullandığınızı belirtmeyi unutmayın.
  • Test edin: Plugin’inizi basit bir test playbook’u ile her değişiklikten sonra çalıştırın. Zira plugin hatası bazen çok gizemli hata mesajları üretir.

Plugin’inizi Test Etmek

Yazdığınız plugin’i test etmek için küçük bir playbook hazırlayın:

# test_playbook.yml
---
- name: Callback plugin test
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Basarili task
      command: echo "Merhaba"

    - name: Kasitli basarisiz task
      command: /bin/false
      ignore_errors: true

    - name: Kosullu atlanan task
      command: echo "Bu atlanacak"
      when: false

    - name: Uzun suren task simulasyonu
      command: sleep 5

    - name: Son task
      debug:
        msg: "Plugin testi tamamlandi"

Çalıştırın ve çıktıyı inceleyin:

ansible-playbook test_playbook.yml -i "localhost," -c local

Eğer log dosyasına yazan bir plugin geliştirdiyseniz:

ansible-playbook test_playbook.yml -i "localhost," -c local && 
  cat /tmp/ansible_tasks.log

Sonuç

Callback Plugin mekanizması, Ansible’ın en az konuşulan ama en güçlü özelliklerinden biri. “Sadece çalışsın yeter” yaklaşımından “tam olarak ne olduğunu bilmek istiyorum” yaklaşımına geçişin anahtarı bu.

Küçük başlamak en doğrusu: önce profile_tasks gibi hazır bir plugin’i aktif edin ve hangi task’larınızın ne kadar sürdüğünü görün. Sonra ihtiyaçlarınıza göre kendi plugin’inizi yazmaya başlayın. Basit bir log yazıcıyla başlayın, sonra buna Slack bildirimi ekleyin, sonra belki bir metrik sistemiyle entegre edin.

Production ortamında Ansible kullanan her ekip er ya da geç şu soruyla yüzleşir: “Dün gece hangi playbook ne yaptı?” İşte bu sorunun cevabını önceden hazırlamak, yani çalışma çıktısını düzgün bir şekilde kayıt altına almak, callback plugin’lerin size kazandırdığı en büyük değer. Bir sonraki olay müdahalesi sırasında “keşke loglamış olsaydım” demek yerine, tüm geçmişe dakikalar içinde ulaşabilirsiniz.

Yorum yapın