Python ile Yapılandırma Dosyası Okuma: configparser ve yaml Kullanımı

Sistem yönetiminde en sık karşılaşılan sorunlardan biri, script’lerin içine gömülü sabit değerlerdir. IP adresi, port numarası, kullanıcı adı, dizin yolu… Bunları her değişiklikte kodun içinde aramak hem zaman kaybıdır hem de hata riskini artırır. Yapılandırma dosyaları bu sorunu elegantly çözer ve Python’un configparser ile yaml kütüphaneleri bu dosyaları okumak için güçlü araçlar sunar.

Bu yazıda gerçek dünya senaryoları üzerinden gideceğiz. Yedekleme script’i, servis monitörü, deployment aracı gibi sysadmin’in günlük işlerine dokunan örnekler kullanacağız.

Neden Yapılandırma Dosyası Kullanmalıyız?

Script’in içine değer gömmek, yani “hardcode” etmek, küçük projeler için kabul edilebilir görünse de zaman içinde kabus haline gelir. Şöyle düşün: Üretim ortamında 20 sunucuyu izleyen bir script yazdın. Monitoring sunucusunun IP’si değişti. Şimdi o script’i bulup içini açman, doğru satırı bulman ve değiştirmen gerekiyor. Peki ya o script’i başka biri yazmışsa ve yorum satırı yoksa?

Yapılandırma dosyalarının avantajları:

  • Kod ve veri ayrımı: Script mantığı değişmeden konfigürasyon güncellenebilir
  • Versiyon kontrolü: Config dosyaları ayrı yönetilebilir, gizli değerler .gitignore‘a alınabilir
  • Operasyon kolaylığı: Sysadmin, Python bilmeden config değiştirebilir
  • Ortam yönetimi: dev.conf, prod.conf, staging.conf ile farklı ortamlar kolayca yönetilir
  • Denetlenebilirlik: Hangi değerin ne zaman değiştiği git history’den takip edilebilir

configparser: INI Formatının Ustası

configparser, Python’un standart kütüphanesinde gelir, kurulum gerektirmez. Windows INI formatını temel alır ve sysadmin’lerin zaten aşina olduğu bir yapı sunar.

Temel INI Formatı

cat /etc/myapp/backup.conf
[general]
app_name = BackupBot
log_level = INFO
log_file = /var/log/backupbot.log

[database]
host = 192.168.1.100
port = 5432
name = production_db
user = backup_user
password = s3cur3p4ss

[storage]
backup_dir = /mnt/nas/backups
retention_days = 30
compress = true
max_size_gb = 500

[notification]
smtp_host = mail.company.com
smtp_port = 587
recipients = [email protected],[email protected]

configparser ile Temel Okuma

import configparser
import sys

def load_config(config_path):
    config = configparser.ConfigParser()
    
    # Dosya okunamazsa hata ver
    files_read = config.read(config_path)
    if not files_read:
        print(f"HATA: Config dosyasi okunamadi: {config_path}")
        sys.exit(1)
    
    return config

def main():
    config = load_config('/etc/myapp/backup.conf')
    
    # String okuma
    app_name = config['general']['app_name']
    log_file = config['general']['log_file']
    
    # Integer okuma - getint() kullan!
    port = config.getint('database', 'port')
    retention_days = config.getint('storage', 'retention_days')
    
    # Boolean okuma - getboolean() kullan!
    compress = config.getboolean('storage', 'compress')
    
    # Float okuma
    max_size = config.getfloat('storage', 'max_size_gb')
    
    # Varsayilan deger ile okuma
    timeout = config.get('database', 'timeout', fallback='30')
    
    print(f"Uygulama: {app_name}")
    print(f"DB Port: {port} (tip: {type(port)})")
    print(f"Sikistirma: {compress} (tip: {type(compress)})")
    print(f"Timeout (varsayilan): {timeout}")

if __name__ == '__main__':
    main()

Burada dikkat etmen gereken kritik nokta: configparser her şeyi string olarak okur. getint(), getboolean(), getfloat() metodlarını kullanmazsan sayısal karşılaştırmalar yanlış sonuç verir. "30" > "9" ifadesi string karşılaştırmasında False döner çünkü "3" < "9". Bu tür ince hatalar production’da saatlerce zaman çalabilir.

Gerçek Dünya: Çok Ortamlı Yedekleme Script’i

Şimdi gerçekçi bir senaryo kuralım. Aynı script’in hem geliştirme hem de üretim ortamında çalışması gerekiyor.

import configparser
import os
import sys
import logging
from datetime import datetime
from pathlib import Path

class BackupManager:
    def __init__(self, env='production'):
        self.env = env
        self.config = self._load_config()
        self._setup_logging()
    
    def _load_config(self):
        config = configparser.ConfigParser()
        
        # Oncelik sirasi: ortam ozgul > genel > varsayilan
        config_files = [
            '/etc/backupbot/defaults.conf',
            f'/etc/backupbot/{self.env}.conf',
            os.path.expanduser(f'~/.backupbot/{self.env}.conf')
        ]
        
        read_files = config.read(config_files)
        
        if not read_files:
            raise FileNotFoundError(
                f"Hic bir config dosyasi okunamadi: {config_files}"
            )
        
        print(f"Yuklenen config dosyalari: {read_files}")
        return config
    
    def _setup_logging(self):
        log_file = self.config.get('general', 'log_file', 
                                    fallback='/var/log/backupbot.log')
        log_level = self.config.get('general', 'log_level', 
                                     fallback='INFO')
        
        logging.basicConfig(
            level=getattr(logging, log_level),
            format='%(asctime)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler(log_file),
                logging.StreamHandler()
            ]
        )
        self.logger = logging.getLogger(__name__)
    
    def get_db_config(self):
        return {
            'host': self.config['database']['host'],
            'port': self.config.getint('database', 'port'),
            'name': self.config['database']['name'],
            'user': self.config['database']['user'],
            'password': self.config['database']['password']
        }
    
    def get_recipients(self):
        recipients_str = self.config.get('notification', 'recipients', fallback='')
        return [r.strip() for r in recipients_str.split(',') if r.strip()]
    
    def run_backup(self):
        db = self.get_db_config()
        backup_dir = self.config['storage']['backup_dir']
        compress = self.config.getboolean('storage', 'compress')
        
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        backup_file = f"{backup_dir}/{db['name']}_{timestamp}.sql"
        
        self.logger.info(f"Yedekleme basliyor: {db['host']}:{db['port']}/{db['name']}")
        
        if compress:
            backup_file += '.gz'
            cmd = (f"pg_dump -h {db['host']} -p {db['port']} "
                   f"-U {db['user']} {db['name']} | gzip > {backup_file}")
        else:
            cmd = (f"pg_dump -h {db['host']} -p {db['port']} "
                   f"-U {db['user']} {db['name']} > {backup_file}")
        
        self.logger.info(f"Komut: {cmd}")
        return backup_file

if __name__ == '__main__':
    env = sys.argv[1] if len(sys.argv) > 1 else 'production'
    manager = BackupManager(env=env)
    manager.run_backup()

Bu yapıyı çalıştırmak için:

# Production ortami
python3 backup.py production

# Development ortami
python3 backup.py development

# Varsayilan (production)
python3 backup.py

YAML: Karmaşık Yapılar için Doğal Seçim

YAML (YAML Ain’t Markup Language), hiyerarşik ve karmaşık yapılar için INI’den çok daha uygun. Özellikle liste, iç içe dictionary ve çok satırlı değerler söz konusu olduğunda YAML’ın gücü ortaya çıkıyor.

Kurulum

# PyYAML kurulumu
pip3 install pyyaml

# Veya sistem paket yoneticisi ile
apt install python3-yaml    # Debian/Ubuntu
dnf install python3-pyyaml  # RHEL/Fedora

Kapsamlı YAML Yapılandırması

# /etc/monitoring/config.yaml

general:
  app_name: "ServerMonitor"
  version: "2.1.0"
  log_level: "INFO"
  log_file: "/var/log/servermonitor.log"
  check_interval: 60  # saniye

servers:
  - name: "web-01"
    host: "192.168.1.10"
    port: 22
    role: "webserver"
    tags:
      - production
      - frontend
    checks:
      - http
      - disk
      - cpu
  
  - name: "db-01"
    host: "192.168.1.20"
    port: 5432
    role: "database"
    tags:
      - production
      - critical
    checks:
      - postgres
      - disk
      - memory

thresholds:
  cpu_warning: 75
  cpu_critical: 90
  memory_warning: 80
  memory_critical: 95
  disk_warning: 85
  disk_critical: 95

notification:
  channels:
    slack:
      enabled: true
      webhook_url: "https://hooks.slack.com/services/xxx/yyy/zzz"
      channel: "#ops-alerts"
    email:
      enabled: true
      smtp_host: "mail.company.com"
      smtp_port: 587
      use_tls: true
      from_address: "[email protected]"
      to_addresses:
        - "[email protected]"
        - "[email protected]"
    pagerduty:
      enabled: false
      api_key: ""
      service_key: ""

maintenance_windows:
  - name: "Haftalik bakim"
    day: "sunday"
    start: "02:00"
    end: "04:00"
  - name: "Aylik guncelleme"
    day: "first-saturday"
    start: "00:00"
    end: "06:00"

YAML Okuma ve Tip Guvenligi

import yaml
import sys
from pathlib import Path

def load_yaml_config(config_path):
    path = Path(config_path)
    
    if not path.exists():
        raise FileNotFoundError(f"Config dosyasi bulunamadi: {config_path}")
    
    if not path.is_file():
        raise ValueError(f"Gecersiz dosya yolu: {config_path}")
    
    with open(path, 'r', encoding='utf-8') as f:
        try:
            config = yaml.safe_load(f)
        except yaml.YAMLError as e:
            print(f"YAML parse hatasi: {e}")
            sys.exit(1)
    
    if config is None:
        raise ValueError("Config dosyasi bos")
    
    return config

def validate_config(config):
    required_sections = ['general', 'servers', 'thresholds', 'notification']
    
    for section in required_sections:
        if section not in config:
            raise KeyError(f"Zorunlu bolum eksik: '{section}'")
    
    if not config['servers']:
        raise ValueError("En az bir sunucu tanimlanmali")
    
    thresholds = config['thresholds']
    if thresholds['cpu_warning'] >= thresholds['cpu_critical']:
        raise ValueError("CPU warning esigi, critical esiginden kucuk olmali")
    
    return True

def main():
    config = load_yaml_config('/etc/monitoring/config.yaml')
    
    try:
        validate_config(config)
    except (KeyError, ValueError) as e:
        print(f"Config dogrulama hatasi: {e}")
        sys.exit(1)
    
    # Genel ayarlar - YAML tipler otomatik
    interval = config['general']['check_interval']
    print(f"Kontrol araligi: {interval}s (tip: {type(interval)})")  # int gelir!
    
    # Sunucu listesinde dongu
    for server in config['servers']:
        print(f"nSunucu: {server['name']}")
        print(f"  Host: {server['host']}:{server['port']}")
        print(f"  Rol: {server['role']}")
        print(f"  Etiketler: {', '.join(server['tags'])}")
        print(f"  Kontroller: {', '.join(server['checks'])}")
    
    # Bildirim kanallarini kontrol et
    notif = config['notification']['channels']
    for channel, settings in notif.items():
        status = "aktif" if settings['enabled'] else "pasif"
        print(f"nBildirim kanali: {channel} - {status}")

if __name__ == '__main__':
    main()

YAML’ın en güzel yanlarından biri tip dönüşümlerini otomatik yapması. 60 yazdığında int, true yazdığında bool, 3.14 yazdığında float olarak gelir. Bu configparser’a kıyasla büyük kolaylık sağlar.

Önemli uyarı: yaml.load() yerine mutlaka yaml.safe_load() kullan. yaml.load(), arbitrary Python nesneleri çalıştırabilir ve güvenlik açığı oluşturur. yaml.safe_load() sadece temel Python tiplerini yükler.

Gerçek Dünya: Servis İzleme Sistemi

import yaml
import logging
import subprocess
import socket
from datetime import datetime, time
from typing import List, Dict, Optional

class ServiceMonitor:
    def __init__(self, config_path: str):
        self.config = self._load_and_validate(config_path)
        self.logger = self._setup_logging()
        self.alerts = []
    
    def _load_and_validate(self, config_path: str) -> dict:
        with open(config_path, 'r', encoding='utf-8') as f:
            config = yaml.safe_load(f)
        
        # Zorunlu alan kontrolu
        assert 'servers' in config, "servers bolumu zorunlu"
        assert 'thresholds' in config, "thresholds bolumu zorunlu"
        
        return config
    
    def _setup_logging(self) -> logging.Logger:
        log_config = self.config.get('general', {})
        
        logger = logging.getLogger('ServiceMonitor')
        logger.setLevel(log_config.get('log_level', 'INFO'))
        
        handler = logging.FileHandler(
            log_config.get('log_file', '/var/log/monitor.log')
        )
        handler.setFormatter(
            logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
        )
        logger.addHandler(handler)
        
        return logger
    
    def check_port(self, host: str, port: int, timeout: int = 5) -> bool:
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(timeout)
            result = sock.connect_ex((host, port))
            sock.close()
            return result == 0
        except socket.error:
            return False
    
    def get_critical_servers(self) -> List[Dict]:
        return [
            s for s in self.config['servers'] 
            if 'critical' in s.get('tags', [])
        ]
    
    def get_servers_by_role(self, role: str) -> List[Dict]:
        return [
            s for s in self.config['servers']
            if s.get('role') == role
        ]
    
    def is_maintenance_window(self) -> bool:
        windows = self.config.get('maintenance_windows', [])
        now = datetime.now()
        
        for window in windows:
            if window['day'].lower() == now.strftime('%A').lower():
                start = time(*map(int, window['start'].split(':')))
                end = time(*map(int, window['end'].split(':')))
                
                if start <= now.time() <= end:
                    self.logger.info(
                        f"Bakim penceresi aktif: {window['name']}"
                    )
                    return True
        
        return False
    
    def run_checks(self):
        if self.is_maintenance_window():
            self.logger.info("Bakim penceresi - kontroller atlaniyor")
            return
        
        thresholds = self.config['thresholds']
        
        for server in self.config['servers']:
            self.logger.info(f"Kontrol ediliyor: {server['name']}")
            
            # Port kontrolu
            port_ok = self.check_port(server['host'], server['port'])
            if not port_ok:
                alert = {
                    'server': server['name'],
                    'type': 'port_check',
                    'severity': 'critical' if 'critical' in server.get('tags', []) else 'warning',
                    'message': f"{server['host']}:{server['port']} erisilemez"
                }
                self.alerts.append(alert)
                self.logger.error(f"HATA: {alert['message']}")
        
        self.logger.info(f"Kontrol tamamlandi. {len(self.alerts)} uyari.")
        return self.alerts

if __name__ == '__main__':
    monitor = ServiceMonitor('/etc/monitoring/config.yaml')
    alerts = monitor.run_checks()
    
    critical_servers = monitor.get_critical_servers()
    print(f"Kritik sunucu sayisi: {len(critical_servers)}")
    
    web_servers = monitor.get_servers_by_role('webserver')
    print(f"Web sunucusu sayisi: {len(web_servers)}")

configparser ve YAML’ı Birlikte Kullanmak

Bazen her ikisini birden kullanmak mantıklı olabilir. INI dosyaları basit sistem yapılandırmaları için, YAML ise karmaşık uygulama yapılandırmaları için ayrı tutulabilir.

import configparser
import yaml
import os
from pathlib import Path

class ConfigManager:
    """Hem INI hem YAML destekleyen genel amacli config yoneticisi."""
    
    def __init__(self, config_dir: str = '/etc/myapp'):
        self.config_dir = Path(config_dir)
        self._ini_config = None
        self._yaml_config = None
    
    def load_ini(self, filename: str = 'system.conf') -> configparser.ConfigParser:
        path = self.config_dir / filename
        config = configparser.ConfigParser()
        
        if not config.read(str(path)):
            raise FileNotFoundError(f"INI config bulunamadi: {path}")
        
        self._ini_config = config
        return config
    
    def load_yaml(self, filename: str = 'app.yaml') -> dict:
        path = self.config_dir / filename
        
        with open(path, 'r', encoding='utf-8') as f:
            config = yaml.safe_load(f)
        
        self._yaml_config = config
        return config
    
    def get(self, key: str, section: str = None, fallback=None):
        """Unified getter - once INI'de, sonra YAML'da ara."""
        
        # INI'de ara
        if self._ini_config and section:
            try:
                return self._ini_config.get(section, key)
            except (configparser.NoSectionError, configparser.NoOptionError):
                pass
        
        # YAML'da ara (nokta notasyonu destekle)
        if self._yaml_config:
            keys = key.split('.')
            value = self._yaml_config
            try:
                for k in keys:
                    value = value[k]
                return value
            except (KeyError, TypeError):
                pass
        
        return fallback
    
    def merge_configs(self) -> dict:
        """Tum config'leri tek dict'te birlestir."""
        result = {}
        
        if self._ini_config:
            for section in self._ini_config.sections():
                result[section] = dict(self._ini_config[section])
        
        if self._yaml_config:
            # YAML degerleri INI'yi ezer
            result.update(self._yaml_config)
        
        return result


# Kullanim ornegi
manager = ConfigManager('/etc/myapp')

try:
    manager.load_ini('system.conf')
    manager.load_yaml('app.yaml')
except FileNotFoundError as e:
    print(f"Config yuklenemedi: {e}")
    exit(1)

# Nokta notasyonu ile YAML'dan oku
smtp_host = manager.get('notification.channels.email.smtp_host')
db_port = manager.get('port', section='database')

print(f"SMTP: {smtp_host}")
print(f"DB Port: {db_port}")

Ortam Değişkenleri ile Entegrasyon

Production ortamlarında şifreler ve API anahtarları config dosyasına yazılmamalı. Bunun için ortam değişkenleri ile config’i birleştiren bir pattern kullanabiliriz:

import yaml
import os
import re

def load_yaml_with_env(config_path: str) -> dict:
    """
    YAML dosyasini yukle ve ${ENV_VAR} formatindaki 
    degerleri ortam degiskenleriyle degistir.
    """
    with open(config_path, 'r', encoding='utf-8') as f:
        content = f.read()
    
    # ${VARIABLE_NAME} veya ${VARIABLE_NAME:default} formatini isle
    pattern = re.compile(r'${([^}:]+)(?::([^}]*))?}')
    
    def replace_env(match):
        var_name = match.group(1)
        default_value = match.group(2)
        
        value = os.environ.get(var_name, default_value)
        
        if value is None:
            raise ValueError(
                f"Ortam degiskeni tanimli degil ve varsayilan yok: {var_name}"
            )
        
        return value
    
    content = pattern.sub(replace_env, content)
    return yaml.safe_load(content)


# config.yaml icerigi su sekilde olabilir:
# database:
#   host: ${DB_HOST:localhost}
#   port: ${DB_PORT:5432}
#   password: ${DB_PASSWORD}   # Varsayilan yok, zorunlu

# Kullanim:
# export DB_HOST=192.168.1.100
# export DB_PASSWORD=supersecret
# python3 myapp.py

config = load_yaml_with_env('/etc/myapp/config.yaml')

Bu pattern Docker ve Kubernetes ortamlarında çok işe yarar. Secret’ları environment variable olarak inject edip config dosyasında referans verebilirsin.

Hata Ayıklama ve Best Practice’ler

Yapılandırma yönetiminde öğrendiğim birkaç önemli nokta:

  • Config dosyasını uygulama başlangıcında doğrula: Sonradan çıkacak hataların önüne geç, fail-fast prensibi uygula
  • Anlamlı hata mesajları yaz: “Config hatası” yerine “database.port değeri integer olmalı, ‘5432abc’ geçersiz” daha kullanışlı
  • Varsayılan değerleri belgele: Her parametrenin varsayılan değeri ve alabileceği değerler açıkça belirtilmeli
  • YAML için unicode kullan: Türkçe karakter içerebilecek dosyalar için encoding='utf-8' şart
  • Config dosyasına şifre yazma: Ortam değişkenleri veya HashiCorp Vault gibi secret manager kullan
  • Config değişikliklerini logla: Hangi config dosyasından hangi değerin okunduğunu kaydet, debug’ı kolaylaştırır
  • Readonly erişim: Config dosyaları chmod 640 ve chown root:appuser ile korun
  • Test ortamı config’i: tests/ klasörüne örnek config dosyaları koy, CI/CD’de bu dosyaları kullan
# Config dosyasi izinleri
sudo chown root:myapp /etc/myapp/config.yaml
sudo chmod 640 /etc/myapp/config.yaml

# Syntax kontrolu - deploy oncesi
python3 -c "import yaml; yaml.safe_load(open('config.yaml'))" && echo "OK"

Sonuç

configparser ve yaml kütüphaneleri, Python ile sistem otomasyonu yazarken olmazsa olmaz araçlar. Hangisini seçeceğin kullanım senaryona göre değişir.

configparserı tercih et:

  • Basit anahtar-değer yapıları yeterliyse
  • Sysadmin’ler tarafından okunup düzenlenecekse
  • Python standart kütüphanesi dışına çıkmak istemiyorsan

yamlı tercih et:

  • Listeler ve iç içe yapılar gerekiyorsa
  • Uygulama konfigürasyonu karmaşıksa
  • Kubernetes, Ansible gibi araçlarla entegrasyon yapıyorsan

En önemli nokta şu: Script’ine ne kadar az sabit değer yazarsan, o script o kadar uzun ömürlü olur. Bir config satırı değiştirmek için Python kodu açmak zorunda kalmamak, sysadmin’in en temel konforudur. Bunu bir alışkanlık haline getirince, geceleri production’da acil düzeltme yapma gereksinimin önemli ölçüde azalacaktır.

Yorum yapın