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.confile 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 640vechown root:appuserile 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.