MikroTik DevOps ve Otomasyon Rehberi: Container, CHR, VPN Mesh ve Zero Trust
MikroTik DevOps ve otomasyon rehberi: container, CHR, çoklu şube VPN mesh, Ansible ile scripting, zero trust mimarisi ve üretim ortamı firewall pratikleri.
Devamını Oku →
Site-to-site VPN bir tane şube için kolaydır: iki router, iki tünel, bitti. Üç şube için biraz daha karmaşıklaşır: hub-and-spoke mu, full mesh mi? On şube? Yirmi? Elli? Bu noktada manuel yapılandırma artık sadece sürdürülemez değil, aynı zamanda üretim riski haline gelir. Bir şube router’ında yanlış girilen tek bir route, bir saat içinde tüm corporate trafiğinin çökmesine yol açabilir. Bu yazıda kurulumu DEĞİL, ölçeği ele alıyoruz: MikroTik tabanlı çoklu şubeli site-to-site VPN mesh topolojinizi Ansible ve scripting ile nasıl otomatize edeceğinizi anlatıyorum.
Yıllar içinde küçük (3-5 şube) ve büyük (80+ şube) müşterilerle çalıştım. Aradaki temel fark, kurulan tünellerin kalitesi değil; yeni bir şubenin ne kadar sürede mesh’e dahil olabildiğidir. Manuel yöntemle 2-3 saat süren bu süreç, doğru otomasyonla 90 saniyeye iner. Bu yazıdaki tüm örnekler production’da kullandığım gerçek pattern’lerin sadeleştirilmiş versiyonlarıdır.
Otomasyona girmeden önce hangi topolojinin hangi ölçekte daha uygun olduğunu kararlaştırmak gerekir; çünkü otomasyon kodunuz bu topolojinin şeklini izleyecek.
Pratik tavsiye: 15 şubeye kadar tek-hub hub-and-spoke yeterlidir. 15-50 arası iki regional hub + spoke yapısı. 50+ için ya tam DMVPN benzeri çözüm (sıklıkla Cisco DMVPN tercih edilir) ya da WireGuard tabanlı mesh koordinatörler (Headscale, Netbird). Bu yazıdaki örneklerimizde hub-spoke + regional mesh üzerinden yürüyeceğiz.
Otomasyonun ilk adımı şubeleri kod yerine veri olarak tanımlamaktır. Aşağıdaki YAML inventory tüm playbook’larımızın kaynağı olur:
# inventory/group_vars/all.yml
corp_asn: 65000
wg_port: 51820
wg_subnet: "10.99.0.0/16"
hubs:
- { name: hub-fra, public_ip: 1.2.3.4, wg_ip: "10.99.0.1", region: "eu-central" }
- { name: hub-iad, public_ip: 5.6.7.8, wg_ip: "10.99.0.2", region: "us-east" }
branches:
- { name: br-istanbul, id: 10, public_ip: 10.0.10.1, wg_ip: "10.99.10.1", lan: "10.10.10.0/24", region: "eu-central", hub: "hub-fra" }
- { name: br-ankara, id: 11, public_ip: 10.0.11.1, wg_ip: "10.99.11.1", lan: "10.10.11.0/24", region: "eu-central", hub: "hub-fra" }
- { name: br-izmir, id: 12, public_ip: 10.0.12.1, wg_ip: "10.99.12.1", lan: "10.10.12.0/24", region: "eu-central", hub: "hub-fra" }
- { name: br-newyork, id: 20, public_ip: 10.0.20.1, wg_ip: "10.99.20.1", lan: "10.10.20.0/24", region: "us-east", hub: "hub-iad" }
- { name: br-chicago, id: 21, public_ip: 10.0.21.1, wg_ip: "10.99.21.1", lan: "10.10.21.0/24", region: "us-east", hub: "hub-iad" }
Yeni şube eklemek tek bir YAML satırı eklemek demektir. Tüm yapılandırma bu kaynaktan türetilir; tek doğru veri kaynağı (single source of truth) ilkesi tam burada uygulanır.
Şimdi yukarıdaki inventory’yi tüketen Ansible playbook’larını yazalım. Önce hub konfigürasyonu:
# roles/mikrotik_hub/tasks/main.yml
- name: Configure WireGuard interface on hub
community.routeros.command:
commands:
- "/interface/wireguard add name=wg-corp listen-port={{ wg_port }} private-key="{{ hub_wg_priv }}""
- "/ip/address add address={{ inventory_hostname_short_ip }}/16 interface=wg-corp"
- name: Add peer entries for each branch in this hub's region
community.routeros.command:
commands:
- >-
/interface/wireguard/peers add
interface=wg-corp
public-key="{{ item.wg_pub }}"
allowed-address={{ item.wg_ip }}/32,{{ item.lan }}
comment="branch-{{ item.id }}-{{ item.name }}"
loop: "{{ branches | selectattr('hub','equalto', inventory_hostname) | list }}"
loop_control: { label: "{{ item.name }}" }
- name: Static routes to branch LANs
community.routeros.command:
commands:
- >-
/ip/route add
dst-address={{ item.lan }}
gateway={{ item.wg_ip }}
comment="auto-{{ item.name }}"
loop: "{{ branches | selectattr('hub','equalto', inventory_hostname) | list }}"
loop_control: { label: "{{ item.name }}" }
Şube tarafı playbook’u:
# roles/mikrotik_branch/tasks/main.yml
- name: WireGuard interface
community.routeros.command:
commands:
- "/interface/wireguard add name=wg-corp listen-port={{ wg_port }} private-key="{{ branch_wg_priv }}""
- "/ip/address add address={{ branch_wg_ip }}/16 interface=wg-corp"
- name: Peer to assigned hub
vars:
my_hub: "{{ hubs | selectattr('name','equalto', assigned_hub) | first }}"
community.routeros.command:
commands:
- >-
/interface/wireguard/peers add
interface=wg-corp
public-key="{{ hub_wg_pubs[my_hub.name] }}"
endpoint-address={{ my_hub.public_ip }} endpoint-port={{ wg_port }}
allowed-address=0.0.0.0/0
persistent-keepalive=25s
comment="hub-{{ my_hub.name }}"
- name: Route corporate prefix via hub
community.routeros.command:
commands:
- "/ip/route add dst-address={{ wg_subnet }} gateway={{ my_hub.wg_ip }} comment=auto-corp"
Şimdi tek bir komut ile her şey hazır:
ansible-playbook -i inventory/ playbooks/corp_mesh.yml --limit "hubs:branches"
3 hub + 50 şube için yapılandırma dakika cinsinden değil, saniye cinsinden tamamlanır. Yeni bir şube eklendiğinde sadece o şubeyi --limit br-yeni ile çalıştırmak ve hub’ları yeniden çalıştırmak yeterlidir.
50 şube = 50 private key + 50 public key + en az 3 hub key. Bu anahtarları nasıl yöneteceğiniz hayati önemde. Üç pratik yöntem:
host_vars/br-istanbul.yml dosyası içinde branch_wg_priv: !vault | ... şifreli olarak saklanır. Master password CI/CD secret’i olarak verilir. Basit, etkili.community.hashi_vault.vault_kv2_get ile çeker. Audit log, dynamic credentials, MFA destekli.Önerim: 10 şubeye kadar Ansible Vault, üzeri için SOPS + age veya HashiCorp Vault.
Yeni bir şube açıldığında WireGuard key çifti üretmek manuel iş olmamalı. Aşağıdaki küçük Python script bunu inventory’ye entegre eder:
#!/usr/bin/env python3
"""new_branch.py - Generate keys + inventory entry for a new branch."""
import subprocess, sys, yaml, os, secrets
from pathlib import Path
if len(sys.argv) < 5:
print("Usage: new_branch.py [hub_name]")
sys.exit(1)
name, bid, pub_ip, lan = sys.argv[1:5]
hub = sys.argv[5] if len(sys.argv) > 5 else "hub-fra"
# Generate WG keys
priv = subprocess.run(['wg', 'genkey'], capture_output=True, text=True).stdout.strip()
pub = subprocess.run(['wg', 'pubkey'], input=priv, capture_output=True, text=True).stdout.strip()
# Append to inventory
inv = Path('inventory/group_vars/all.yml')
data = yaml.safe_load(inv.read_text())
data['branches'].append({
'name': name, 'id': int(bid),
'public_ip': pub_ip, 'wg_ip': f"10.99.{bid}.1",
'lan': lan, 'region': 'auto', 'hub': hub,
'wg_pub': pub
})
inv.write_text(yaml.dump(data, default_flow_style=False))
# Encrypt private key with ansible-vault and write to host_vars
host_vars = Path(f'inventory/host_vars/{name}.yml')
plain = f"branch_wg_priv: "{priv}"n"
host_vars.write_text(plain)
subprocess.run(['ansible-vault', 'encrypt', str(host_vars)])
# Commit to git
subprocess.run(['git', 'add', str(inv), str(host_vars)])
subprocess.run(['git', 'commit', '-m', f'Add branch {name} (id={bid})'])
print(f"OK: {name} added. Run ansible-playbook to deploy.")
Tek komut: ./new_branch.py br-bursa 13 203.0.113.13 10.10.13.0/24 hub-fra. Sonra bir git push + CI pipeline tetikleme, yapılandırma 2 dakikada canlı.
RouterOS 7.x REST API sunar (HTTPS üzerinden JSON). Ansible alternatifi olarak direkt Python script’leri ile de aynı işi yapabilirsiniz. Bu özellikle ad-hoc operasyonlar için kullanışlıdır:
#!/usr/bin/env python3
"""bulk_peer_add.py - Add multiple WG peers to a MikroTik via REST API."""
import requests, json, yaml, sys
from requests.auth import HTTPBasicAuth
requests.packages.urllib3.disable_warnings()
ROUTER_IP = sys.argv[1]
USER = "automation-bot"
PASS = os.environ["MTK_PASS"]
BASE = f"https://{ROUTER_IP}/rest"
# Load peers definition
with open(sys.argv[2]) as f:
peers = yaml.safe_load(f)
for p in peers:
body = {
"interface": "wg-corp",
"public-key": p["pub"],
"allowed-address": ",".join(p["allowed"]),
"comment": p.get("comment", "auto"),
}
if "endpoint" in p:
body["endpoint-address"] = p["endpoint"]
body["endpoint-port"] = str(p.get("port", 51820))
r = requests.put(f"{BASE}/interface/wireguard/peers",
auth=HTTPBasicAuth(USER, PASS),
json=body, verify=False, timeout=10)
if r.status_code in (200, 201):
print(f" + added peer {p['pub'][:12]}...")
else:
print(f" ! failed {p['pub'][:12]}: {r.status_code} {r.text}")
Bu pattern özellikle MikroTik fleet’inin sadece bir kısmını seçici şekilde güncellemek gerektiğinde Ansible’a alternatif olarak iyi çalışır.
Mesh büyüdükçe “Acaba hangi tünel down?” sorusu sürekli sorulur. Bunu insan gözüyle kovalamak verimsizdir. İki katmanlı monitoring kurmanızı öneririm:
# prometheus.yml extract
scrape_configs:
- job_name: 'mikrotik'
file_sd_configs:
- files: ['/etc/prometheus/targets/mikrotik_*.json']
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: mikrotik-exporter:9436
Targets dosyaları Ansible’dan otomatik render edilir; yeni şube eklendiğinde Prometheus reload yeterli.
Her şubeye küçük bir cron job kurun:
/system scheduler
add name=mesh-probe interval=60s on-event={
:foreach hub in={"10.99.0.1"; "10.99.0.2"} do={
:if ([/ping address=$hub count=2 interval=1s] = 0) do={
/log warning message=("Mesh probe FAIL to " . $hub)
/tool fetch url="https://alerts.example.com/probe-fail"
http-data=("router=" . [/system identity get name] . "&hub=" . $hub)
mode=https
}
}
}
Probe başarısız olduğunda Alertmanager üzerinden PagerDuty veya Slack’e bildirim gider. Önemli: Probe scriptini Ansible ile push edin; manuel girilirse şubeye göre tutarsız olur.
Daha ileri gitmek isterseniz LibreNMS veya Observium gibi platformlar SNMP üzerinden topology auto-discovery yapar. Mesh büyüdükçe Grafana üzerinde “live mesh map” oluşturmak yararlıdır; gerçek zamanlı tünel durumu görselleştirilir.
Otomasyonun en zor problemi: bir admin Winbox açıp manuel değişiklik yaptıysa ne olur? Bu “configuration drift” en sinsi prodüksiyon hatasıdır. Çözüm: reconciliation. Cron ile çalışan bir Ansible run:
# /etc/cron.d/mtk-reconcile
0 */4 * * * ansible /usr/bin/ansible-playbook
-i /opt/mesh/inventory
/opt/mesh/playbooks/corp_mesh.yml
--check --diff > /var/log/mtk-drift.log 2>&1
Burada --check --diff kullandığımıza dikkat: değişiklik yapmadan sadece beklenen ile gerçek durumu kıyaslar. Drift varsa Slack’e bildirim. Aktif düzeltme için aynı komutu --check olmadan çalıştırırsınız ancak bu yaklaşımı staging’de iyice test ettikten sonra production’a alın.
Şube router’larında iki internet bağlantısı varsa (ana fiber + LTE), iki WireGuard tüneli farklı bağlantılardan açılabilir. BGP ile path selection yapılır:
/routing bgp instance
add as=65000 router-id={{ branch_wg_ip }}
/routing bgp peer
add name=hub-primary remote-address=10.99.0.1 remote-as=65000
local-address={{ branch_wg_ip_primary }}
add name=hub-backup remote-address=10.99.0.1 remote-as=65000
local-address={{ branch_wg_ip_lte }}
in-filter=lower-pref
Otomatize edildiğinde, primary link düşerse BGP saniyeler içinde traffic’i backup tünele kaydırır. Önerilen BFD (Bidirectional Forwarding Detection) ile failover süresi 1 saniyenin altına inebilir. RouterOS 7.x BFD’yi destekler.
Her Ansible değişikliği staging’de test edilmeli. Pratik yaklaşım:
Containerlab örneği:
# topo.yml
name: mtk-mesh-test
topology:
nodes:
hub-fra:
kind: linux
image: mikrotikrtr/chr:7.13
br-ist:
kind: linux
image: mikrotikrtr/chr:7.13
br-ank:
kind: linux
image: mikrotikrtr/chr:7.13
links:
- endpoints: ["hub-fra:eth1", "br-ist:eth1"]
- endpoints: ["hub-fra:eth2", "br-ank:eth1"]
containerlab deploy -t topo.yml ile lab ayakta; Ansible’ı bu lab’a yöneltip değişiklik test edebilirsiniz.
Tüm bu parçaları birleştiren bir GitHub Actions workflow örneği:
name: Mesh Deploy
on:
pull_request:
paths: ['inventory/**', 'roles/**', 'playbooks/**']
push:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install ansible-lint yamllint
- run: yamllint inventory/ playbooks/ roles/
- run: ansible-lint playbooks/
dry-run:
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install ansible community.routeros
- run: |
ansible-playbook -i inventory/ playbooks/corp_mesh.yml
--check --diff --vault-password-file <(echo ${VAULT_PASS})
env:
VAULT_PASS: ${{ secrets.ANSIBLE_VAULT_PASS }}
apply:
needs: dry-run
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- run: |
ansible-playbook -i inventory/ playbooks/corp_mesh.yml
--vault-password-file <(echo ${VAULT_PASS})
env:
VAULT_PASS: ${{ secrets.ANSIBLE_VAULT_PASS }}
PR açıldığında lint + dry-run; merge sonrası gerçek deployment. Production environment'a manuel approval ekleyebilirsiniz.
/ip settings set ipv4-fast-path=yes. Mesh forwarding 2x hızlanır.Otomasyonun sürdürülebilirliği isimlendirmeden başlar. Standartlaşmış convention:
wg-corp, wg-mgmt, wg-dr (corp/management/disaster recovery)auto-{branch-id}-{branch-name}. auto- prefix'i ile manuel girilenden ayrılır.auto-{purpose}, örneğin auto-br-istanbul-lanauto-corp-mesh, auto-hub-listAnsible playbook'ları reconciliation sırasında "auto- prefix'li olmayan kuralları" silmez; bu sayede manuel emergency değişikliklere dokunmaz. Aynı zamanda hangi kurallar otomatize edildiği bir bakışta görülür.
WireGuard konfigürasyonu deklaratiftir (her peer self-contained), IPsec ise stateful (IKE müzakeresi + SPD/SAD). Otomasyon için WireGuard 3-4 kat daha az satır kod, 5-10 kat daha hızlı convergence sunar. Performans açısından modern CPU'larda her ikisi de benzerdir. Eğer regulatory uyumluluk IPsec gerektiriyorsa (örneğin bazı bankacılık ortamları), aynı mesh prensipleri IPsec ile de uygulanabilir.
Evet. SaltStack özellikle gerçek zamanlı event-driven mesh için iyidir (her şube bir minion olur, master'dan komutlar push edilir). Puppet daha çok klasik server config management'a uygundur; network device'lar için Ansible kadar yaygın değildir. Pratik yaklaşım: Ansible config push, SaltStack event bus.
İnventory'ye satır eklemek + Ansible run + DNS güncelleme: yaklaşık 5-10 dakika. Fiziksel kurulum (router'ın yerinde olması, internet bağlantısının kurulmuş olması) hariç. Pre-staged routers (DC'den zaten yapılandırılarak gönderilmiş) ile bu süre sıfıra yakındır; şube açılışında plug-and-play.
Hayır. WireGuard tabanlı koordinatörler (Headscale, Netbird, Tailscale Enterprise) modern alternatiftirler. Bunlar control plane'i ayırarak data plane'i WireGuard'a bırakır. MikroTik resmi olarak DMVPN konuşmaz; ancak WireGuard ile benzer bir dinamik mesh kurulabilir. Cisco DMVPN'in cazibesi NHRP standart oluşudur; WireGuard mesh koordinatörlerinde her vendor kendi protokolünü kullanır.
Production için her 4 saatte bir --check mod run yeterli. Critical change window'larında (örneğin maintenance pencerelerinde) daha sık. Drift bildirildiğinde manuel müdahale alarm doğurmalı; tekrar tekrar oluyorsa "manuel değişikliği inventory'ye işle veya geri al" karar verilmeli.
Hub-spoke topolojide hayır; her şubeden çıkan trafik hub'a uğrar sonra hedef şubeye gider. Bu "trombone routing" hub'da double bandwidth tüketir. Sık şube-şube trafiği varsa dinamik peer-to-peer tünel (DMVPN tarzı) veya hub'da intelligent SD-WAN tercih edilir. WireGuard'da spoke-to-spoke trafiği için her iki spoke'un birbirine direct peer eklenmesi yeterlidir.
Her şubenin tüm hub'ların public key'ine ihtiyacı vardır. 3 hub için sorun değil. 30 hub'lık devasa mesh için bir key broker (Vault + dynamic secrets) kullanın; her peer ihtiyacı olan key'leri JIT (just-in-time) çeker. Bu, on-boarding süresini azaltır ve rotated key dağıtımını otomatikleştirir.
Çoklu şubeli MikroTik mesh yönetimi, kurulum tekniğinden çok operasyonel disiplin sorunudur. Doğru inventory tasarımı, Ansible playbook'ları, scripting otomasyonu, monitoring entegrasyonu ve CI/CD pipeline bir araya geldiğinde 50 şubeli mesh, 2 şube kadar yönetilebilir hale gelir. En önemli ders: her şey kod olmalı. Yeni şube bir YAML satırıdır; yeni firewall kuralı bir commit'tir; rollback bir git revert. Bir sonraki yazıda bu mesh yapısının üzerine adaptive firewall, threat intel feed ve zero-trust microsegmentation katmanı eklemeyi inceleyeceğiz.
Bağlantılı kavramları daha derinden işleyen yazılarımız:
Bu yazıdaki konuları derinleştirmek için aşağıdaki otoriter kaynaklara başvurabilirsiniz:
Çok noktalı tünellerde statik route yerine OSPF ile otomatik yol belirleme kullanın:
/routing ospf instance add name=mesh router-id=10.255.0.1
/routing ospf area add name=backbone area-id=0.0.0.0 instance=mesh
/routing ospf interface-template add interfaces=wg0 area=backbone network-type=ptp cost=10
Tünel düşerse OSPF alternatif yolu saniyeler içinde devreye alır. Kurumsal mesh için VPN Kurulum Hizmetimize bakın.