Orchestrating a
Virtual Hub in Go

Container · VM · Ağ · VPN — tek bir binary

Ahmet Türkmen

Systems Development Engineer — AWS

linkedin.com/in/mrturkmen github.com/mrtrkmn

Gophers İstanbul · 2026

$ whoami

Ahmet Türkmen

Systems Development Engineer — AWS

Altyapı, otomasyon, dağıtık sistemler

Go ile günlük araç ve servis geliştirme

Container, VM ve ağ otomasyonu

linkedin.com/in/mrturkmen github.com/mrtrkmn

Gündem (~45 dk)

  1. Neden Go? — Orkestrasyon için Go'nun avantajları
  2. Go ekosistemi — Kubernetes'ten Terraform'a
  3. Mimari genel bakış
  4. net Go'nun ağ (networking) süper gücü
  5. goroutine Ucuz paralellik ile fan-out
  6. channel Güvenli iletişim ve hata toplama
  7. os/exec Altyapı uyumluluğu — virsh, crypto, static binary
  8. sync Thread-safe veri yapıları
  9. Demo & Soru-Cevap

Odak: Go nasıl yazılır değil — Go'nun hangi özellikleri altyapı yazılımını doğal ve güçlü kılıyor.

1 — Neden Go?

Orkestrasyon yazılımı için Go'yu seçmek

Demo / Lab Ortamları Zordur

SUNUCU (tek makine) nginx:alpine container .5.10 whoami container .5.11 DHCP DNS container .5.2 / .5.3 Debian VM cloud-init · 1024 MB · 2 vCPU curl, wget, vim, htop, nmap DHCP ile IP atanır Docker Bridge Ağı — 172.19.5.0/24 gateway .5.1 · subnet mask 255.255.255.0 · DHCP range .5.4–.5.254 WireGuard VPN Tüneli 10.10.0.0/24 · X25519 anahtar çifti · wg-quick uyumlu Uzak İstemci wg-client.conf Container VM (libvirt/KVM) VPN Tünel
Hedef: tek bir komut ile eksiksiz bir hibrit ortam — tekrarlanabilir, deklaratif, hızlı.

Ne İstiyoruz?

$ orchestrator up -c config.yaml
# → Docker ağı oluşturuldu      (172.19.5.0/24)
# → DHCP & DNS container'ları çalışıyor
# → nginx, whoami container'ları çalışıyor
# → Debian VM cloud-init ile boot edildi
# → WireGuard istemci yapılandırması üretildi

$ orchestrator status    # durum kontrolü
$ orchestrator ssh demo-vm   # VM'e otomatik SSH
$ orchestrator down      # temizleme

Tek binary, sıfır bağımlılık, YAML ile tanımlanmış altyapı.

2 — Go Ekosistemi

Bulut altyapısının fiili dili

Go ile Yazılmış Kritik Projeler

Proje Alan Neden Go?
Kubernetes Orkestrasyon Goroutine'ler ile binlerce pod yönetimi
Docker / containerd Container OS syscall'larına doğrudan erişim
Terraform IaC Plugin mimarisi, statik binary dağıtımı
Prometheus İzleme Yüksek throughput metrik toplama
etcd KV Store Raft consensus, net/rpc
CockroachDB Veritabanı Dağıtık SQL, channel ile koordinasyon
Caddy Web Server Otomatik TLS, net/http stdlib
CoreDNS DNS Plugin chain mimarisi

Ortak tema: hepsi ağ yoğun, eşzamanlı ve statik binary olarak dağıtılıyor.

Kubernetes Her Zaman Cevap Değildir

  • Bazen tek bir sunucuda VM + container birlikte gerekir
  • Kubernetes virsh domain'lerini doğrudan yönetemez
  • ~15 MB Go binary vs. tam bir k8s kümesi
  • Edge / lab / demo senaryoları için biçilmiş kaftan

# Tek komut ile derleme — bağımlılık yok
$ go build -o orchestrator .
$ ls -lh orchestrator
-rwxr-xr-x 1 user user 14M Feb 26 20:00 orchestrator

# İstediğin platforma çapraz derleme
$ GOOS=linux GOARCH=arm64 go build -o orchestrator-arm .

3 — Mimari Genel Bakış

Paket Yapısı

orchestrator/
├── main.go                # giriş noktası
├── cmd/                   # CLI komutları (cobra)
│   ├── root.go            # flag'ler, log ayarları
│   ├── up.go / down.go    # yaşam döngüsü
│   ├── ssh.go             # VM'e interaktif SSH
│   └── log_hook.go        # goroutine-ID loglama
├── config/                # YAML ayrıştırıcı
├── orch/                  # çekirdek orkestratör
│   ├── orchestrator.go    # Up / Down / Status
│   ├── dockerctl/         # Docker API sarıcısı
│   ├── libvirtctl/        # virsh CLI sarıcısı
│   ├── imagebuilder/      # özel VM imaj oluşturucu
│   └── ipool/             # thread-safe IP havuzu
└── wg/                    # WireGuard yapılandırma üretici

Bileşen Akışı

CLI (Cobra)
up · down · status · ssh
Orchestrator
config · state · hata birleştirme
dockerctl
Docker API
container · ağ
DHCP · DNS
libvirtctl
virsh CLI
VM · cloud-init
KVM / QEMU
ipool
IP tahsisi
/8 → /30
thread-safe
wg
WireGuard
X25519 anahtar
config üretimi

Her kutu bağımsız bir Go paketi — açık bağımlılıklar, kolay test.

net Go'nun Ağ Süper Gücü

IPv4 aritmetiği, CIDR ayrıştırma, broadcast hesaplama

CIDR Nedir?

CIDR (Classless Inter-Domain Routing) — IP adreslerini ve alt ağları tanımlamak için kullanılan bir gösterim biçimidir.
CIDR Gösterimi 172 . 19 . 5 . 0 / 24 bit ← 24 bit ağ (network) → 8 bit host Ağ adresi (network address) İlk 24 bit sabit → tüm host'lar aynı ağda Host kısmı 2⁸ − 2 = 254 kullanılabilir adres CIDR Ağ Maskesi Kullanılabilir Host 192.168.1.0/24 255.255.255.0 254 10.42.0.0/16 255.255.0.0 65 534 10.0.0.0/8 255.0.0.0 16 777 214 Host sayısı = 2^(32 − prefix) − 2 (ağ adresi ve broadcast hariç)

Nerede kullanılır? Ağ segmentasyonu, IP havuzu yönetimi, DHCP aralık tanımlama, firewall kuralları, routing tabloları — ağ programlamasının temel yapı taşı.

net.ParseCIDR — Subnet'i Anlamak

📁 orch/ipool/ipool.go — NewIPPool()

func NewIPPool(cidr string) (*IPPool, error) {
    ip, ipnet, err := net.ParseCIDR(cidr)
    if err != nil {
        return nil, err
    }
    ip4 := ip.To4()
    if ip4 == nil {
        return nil, errors.New("only IPv4 addresses are supported")
    }

    ones, bits := ipnet.Mask.Size()
    if bits != 32 || ones > 30 {
        return nil, errors.New("only IPv4 with prefix length /8 to /30 is supported")
    }

    netIP := ipnet.IP.To4()
    base := ipToUint32(netIP)                  // ağ adresi → uint32
    bcast := base | ^maskToUint32(ipnet.Mask)  // broadcast hesaplama

    start := base + 30   // ilk 29 IP altyapıya ayrılmış
    end   := bcast - 1   // son kullanılabilir host
    // ...
}
net.ParseCIDR("172.19.5.0/24") → üç değer döndürür:
1. IP adresi (172.19.5.0), 2. Ağ bilgisi — ağ adresi + maske (172.19.5.0/24), 3. Hata (geçersiz CIDR ise).
Tek satırda tüm subnet bilgisine sahip olursunuz.

IPv4 = uint32 — Anahtar İçgörü

📁 orch/ipool/ipool.go — yardımcı fonksiyonlar

// Her IPv4 adresi aslında 32-bit'lik bir tam sayıdır
func ipToUint32(ip net.IP) uint32 {
    return binary.BigEndian.Uint32(ip.To4())
}

func uint32ToIP(n uint32) net.IP {
    ip := make(net.IP, 4)
    binary.BigEndian.PutUint32(ip, n)
    return ip
}

func maskToUint32(m net.IPMask) uint32 {
    return binary.BigEndian.Uint32(m)
}
Neden uint32? — IP adreslerini sayıya çevirince aralık kontrolü basit matematik olur:
10.42.0.0175_046_656  |  10.42.255.255175_112_191
Adres bu iki sayı arasında mı? → Tek bir if yeter.

Broadcast Hesaplama — Gerçek Kod

📁 orch/dockerctl/client.go — EnsureDHCPAndDNS()

_, ipNet, err := net.ParseCIDR(pool.Subnet())
// ...
netIP := ipNet.IP.To4()
mask := ipNet.Mask

// Broadcast = ağ adresi OR (NOT maske)
broadcastBytes := make([]byte, 4)
for i := 0; i < 4; i++ {
    broadcastBytes[i] = netIP[i] | ^mask[i]
}
broadcast := net.IP(broadcastBytes).String()

// Subnet bilgisinden DHCP yapılandırması üret
maskStr := fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3])
dhcpIP    := pool.FormatIP(2)    // x.x.x.2  → DHCP sunucu
dnsIP     := pool.FormatIP(3)    // x.x.x.3  → DNS sunucu
minRange  := pool.FormatIP(4)    // x.x.x.4  → DHCP aralık başı
maxRange  := pool.FormatIP(pool.BroadcastOffset() - 1)
Ne yapıyor? Subnet CIDR'ından DHCP'nin tüm bilgilerini çıkartıyor:
1. Broadcast hesapla → 172.19.5.255 2. Maske → 255.255.255.0 3. Sabit IP: .2 = DHCP, .3 = DNS 4. Aralık: .4.254 istemcilere
Go'nun net + encoding/binary ile — harici kütüphane sıfır.

DHCP Config Üretimi — fmt.Sprintf ile Şablon

📁 orch/dockerctl/client.go

dhcpConf := fmt.Sprintf(`default-lease-time 600;
max-lease-time 7200;
authoritative;
option domain-name-servers %s;
option routers %s;

subnet %s netmask %s {
  range %s %s;
  option subnet-mask %s;
  option broadcast-address %s;
}
`, dnsIP, gatewayIP, networkAddr, maskStr,
   minRange, maxRange, maskStr, broadcast)

if err := os.WriteFile(dhcpConfPath, []byte(dhcpConf), 0o644); err != nil {
    return "", "", "", err
}

Harici şablon motoru yok — Go'nun backtick string'leri + fmt.Sprintf yeterli.

goroutine Ucuz Paralellik

Container + VM'leri eşzamanlı provizyon etmek

Neden Goroutine?

Sıralı (Yavaş) zaman → C1 C2 VM1 VM2 3s 2s container'lar bitti → VM'ler başlar 8s 5s Toplam = Σ hepsi = 18s 3 + 2 + 8 + 5 Paralel (Goroutine) zaman → go func C1 C2 VM1 VM2 3s 2s 8s 5s done Toplam = max(hepsi) = 8s max(3, 2, 8, 5) — 2.25× daha hızlı
Goroutine başlangıç maliyeti: ~2 KB yığın belleği. Thread: ~1 MB.
10.000 goroutine ≈ 20 MB. 10.000 thread ≈ 10 GB.

İki Seviyeli Fan-Out — Gerçek Kod

"İki seviyeli fan-out" ne demek? Fan-out = tek noktadan birden fazla paralel işe dallanma. Seviye 1: Ana goroutine → 2 gözetici goroutine: biri container'lar, diğeri VM'ler için. Seviye 2: Her gözetici → her container/VM için ayrı goroutine. İki seviye = hem türler arası hem tür içi paralellik.

📁 orch/orchestrator.go — Up() metodu

// ── Container ve VM'leri eşzamanlı başlat ──
var (
    topWG   sync.WaitGroup
    topErrs []error
    topMu   sync.Mutex        // topErrs'i korur
)

// Seviye 1: Container'lar ‖ VM'ler (iki üst-seviye goroutine)
topWG.Add(1)
go func() {                                    // ── Container gözetici ──
    defer topWG.Done()
    var wg sync.WaitGroup
    errChan := make(chan error, len(o.cfg.Containers))
    for _, c := range o.cfg.Containers {
        wg.Add(1)
        go func(container config.ContainerCfg) { // Seviye 2: her container
            defer wg.Done()
            // IP tahsis et, container başlat...
        }(c)
    }
    wg.Wait()
    close(errChan)
    // hataları topla → topErrs
}()

topWG.Add(1)
go func() {                                    // ── VM gözetici ──
    defer topWG.Done()
    // aynı kalıp: her VM için goroutine
}()

topWG.Wait()                                   // bariyer — hepsi bitene kadar bekle

Goroutine İçinde Goroutine — Detay

📁 orch/orchestrator.go — container goroutine'leri

for _, c := range o.cfg.Containers {
    wg.Add(1)
    go func(container config.ContainerCfg) {
        defer wg.Done()

        entry := o.log.WithField("container", container.Name)

        // Statik IP varsa rezerve et, yoksa rastgele tahsis et
        var usedIP string
        if container.IP != "" {
            usedIP = container.IP
            if err := o.pool.ReserveIP(usedIP); err != nil {
                entry.WithError(err).Warn("statik IP rezerve edilemedi")
            }
        } else {
            ip, err := o.pool.RandomIP()
            if err != nil {
                errChan <- fmt.Errorf("IP tahsisi %s: %w", container.Name, err)
                return
            }
            usedIP = ip
        }

        if err := o.dc.RunContainer(ctx, container.Name,
            container.Image, container.Cmd, container.Env,
            label, usedIP, netName); err != nil {
            errChan <- fmt.Errorf("container %s: %w", container.Name, err)
            return
        }
        entry.Info("container başlatıldı")
    }(c)   // ← closure tuzağını önlemek için parametre olarak geç
}

Closure Tuzağı Nedir?

⚠️ Bir for döngüsü içinde go func() { ... }() başlattığınızda, fonksiyon dıştaki c değişkenine referansla bağlanır ("closure" = kapanış).

Sorun: goroutine çalışmaya başladığında döngü çoktan ilerlemiştir, tüm goroutine'ler son değeri görür — yanlış container başlatılır!
Çözüm: go func(container ContainerCfg) { ... }(c)
Döngü değişkenini parametre olarak geçirince o anki değer kopyalanır, her goroutine kendi bağımsız kopyasıyla çalışır.

Not: Go 1.22+ bu sorunu otomatik çözer, ancak eski sürümlerle uyumluluk için bu kalıp hâlâ önerilir.

sync.WaitGroup — Bariyer Kalıbı

  main goroutine                   sayaç = 0
       ├── wg.Add(1) ──────────────sayaç = 1
       │   └── go func A()
       ├── wg.Add(1) ──────────────sayaç = 2
       │   └── go func B()
       ├── wg.Add(1) ──────────────sayaç = 3
       │   └── go func C()
       ▼
   wg.Wait()  ◄── blokla ─────────sayaç > 0
       ·  (A bitti → wg.Done()) ───sayaç = 2
       ·  (C bitti → wg.Done()) ───sayaç = 1
       ·  (B bitti → wg.Done()) ───sayaç = 0  ✓
       ▼
   devam et!  ◄── bariyer açıldı
Bariyer kalıbı: WaitGroup sayaç tutar. Add(1) artırır, Done() azaltır, Wait() sıfıra düşene kadar bloklar.
var wg sync.WaitGroup
wg.Add(len(services))          // sayacı artır
for _, svc := range services {
    go func() {
        defer wg.Done()        // bitince sayacı azalt
        // iş yap...
    }()
}
wg.Wait()                      // sayaç 0 olana kadar bekle

channel Güvenli İletişim

Goroutine'ler arası hata toplama ve senkronizasyon

Buffered Error Channel — Gerçek Kod

📁 orch/orchestrator.go

// Tamponlu kanal: her goroutine kendi hatasını bloklanmadan gönderir
errChan := make(chan error, len(o.cfg.Containers))

go func(container config.ContainerCfg) {
    defer wg.Done()
    if err := o.dc.RunContainer(ctx, ...); err != nil {
        errChan <- fmt.Errorf("container %s: %w", container.Name, err)
        return      // ← kanal tamponlu olduğu için bloklanmaz
    }
}(c)

// Tüm goroutine'ler bittikten sonra kanalı kapat
wg.Wait()
close(errChan)

// range ile tüm hataları topla
var errs []error
for err := range errChan {        // kanal kapanana kadar oku
    errs = append(errs, err)
}
if len(errs) > 0 {
    return errors.Join(errs...)   // Go 1.20+ — birden fazla hatayı birleştir
}

Channel Akışı — Görsel

goroutine-1 (container) goroutine-2 (VM) goroutine-N (servis) ··· err₁ err₂ errₙ errChan := make(chan error, N) e₁ e₂ ·· eₙ buffered (bloklanmaz) wg.Wait() → close(errChan) for err := range errChan { errs = append(errs, err) } errors.Join(errs...)
Neden buffered?make(chan error, N) ile N goroutine hata gönderebilir ve hiçbiri bloklanmaz. Unbuffered olsaydı her gönderim bir alıcı beklerdi.

select — Üç Yollu Bekleme

📁 orch/dockerctl/client.go — waitForRunning()

func (c *Client) waitForRunning(ctx context.Context,
    containerID string, timeout time.Duration) error {

    timer := time.NewTimer(timeout)
    defer timer.Stop()

    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():                              // ① iptal
            return ctx.Err()
        case <-timer.C:                                 // ② zaman aşımı
            return errors.New("container başlatma zaman aşımı")
        case <-ticker.C:                                // ③ yoklama
            ins, err := c.c.InspectContainer(containerID)
            if err != nil { continue }
            if ins.State.Running { return nil }
        }
    }
}
select = Go'nun multiplexer'ı. Birden fazla channel'ı aynı anda dinler, hangisi hazırsa o dalı çalıştırır. Context iptali ücretsiz gelir.

DHCP + DNS — Paralel Servis Başlatma

📁 orch/dockerctl/client.go — EnsureDHCPAndDNS()

type serviceResult struct {
    name string
    id   string
    err  error
}

results := make(chan serviceResult, len(services))
var wg sync.WaitGroup
wg.Add(len(services))
for _, svc := range services {
    svcCopy := svc
    go func() {
        defer wg.Done()
        id, svcErr := c.createStartAndWait(ctx, svcCopy.opts, 30*time.Second)
        results <- serviceResult{name: svcCopy.name, id: id, err: svcErr}
    }()
}
wg.Wait()
close(results)

// Sonuçları topla — başarılı ve başarısız olanları ayır
for res := range results {
    if res.err != nil {
        errs = append(errs, fmt.Errorf("%s: %w", res.name, res.err))
    }
}

Channel struct göndermek — sadece error değil, zengin sonuç nesneleri taşır.

os/exec Altyapı Uyumluluğu

virsh, dosya sistemi, kriptografi — hepsi stdlib'de

Neden libvirt / virsh?

Neden Açıklama
Soyutlama KVM, QEMU, Xen — tek API, standart XML
CGo yok virsh CLI → statik binary korunur, C bağımlılığı sıfır
Zarif geri çekilme /dev/kvm varsa type='kvm', yoksa type='qemu'
Cloud-init ISO'yu CD-ROM olarak bağla → VM ilk açılışta otomatik yapılandırılır
Hibrit ağ VM + container aynı bridge ağında — gerçek kernel izolasyonu
Docker container'ları namespace ile izole eder, libvirt VM'leri ayrı kernel ile — ikisini birleştirmek bu projenin temel değeri.

os/exec — Sistem Komutlarını Yönetmek

📁 orch/libvirtctl/libvirt.go — DefineVM()

func (c *Client) DefineVM(name, imagePath, cloudInitISO,
    network string, memoryMB, vcpus int) error {

    // XML şablonunu Go string'i olarak oluştur
    xml := fmt.Sprintf(`<domain type='%s'>
  <name>%s</name>
  <memory unit='MiB'>%d</memory>
  <vcpu>%d</vcpu>
  ...
</domain>`, virtType(), name, memoryMB, vcpus, ...)

    // virsh'e stdin üzerinden XML gönder
    cmd := exec.Command("virsh", "define", "/dev/stdin")
    cmd.Stdin = strings.NewReader(xml)   // ← string → io.Reader
    output, err := cmd.CombinedOutput()
    if err != nil {
        return fmt.Errorf("VM tanımla: %w, çıktı: %s", err, output)
    }
    return nil
}
exec.Command + cmd.Stdin = harici araca veri boru hattı. Geçici dosya yok, pipe yönetimi yok.

os.Stat — Ortam Algılama

📁 orch/libvirtctl/libvirt.go

// KVMAvailable — donanım sanallaştırma desteğini kontrol et
func KVMAvailable() bool {
    _, err := os.Stat("/dev/kvm")
    return err == nil              // dosya varsa KVM kullanılabilir
}

// Çalışma ortamına göre otomatik strateji seçimi
func virtType() string {
    if KVMAvailable() {
        return "kvm"               // donanım hızlandırma ✓
    }
    return "qemu"                  // yazılım emülasyonu (örn: AWS EC2)
}

📁 orch/orchestrator.go — cloud-init ISO algılama

// Dosya var mı kontrol et — yoksa CD-ROM ekleme
cloudInitISO := filepath.Join(
    filepath.Dir(vm.Image), "..", "cloud-init", "cloud-init.iso",
)
if _, err := os.Stat(cloudInitISO); err != nil {
    cloudInitISO = ""   // cloud-init bulunamadı → CD-ROM'u atla
}
⚠️ Bulut VM'lerinin çoğunda /dev/kvm yoktur — aracınız çökmek yerine zarif bir şekilde geri çekilmelidir.

crypto — Saf Go ile WireGuard Anahtarları

📁 wg/wg.go — GenerateClientConfig()

func GenerateClientConfig(peerName, address string) error {
    // 32 rastgele byte üret — Go'nun crypto/rand paketi
    var priv [32]byte
    if _, err := rand.Read(priv[:]); err != nil {
        return fmt.Errorf("rastgele okuma: %w", err)
    }

    // RFC 7748'e göre clamp — X25519 için gerekli
    priv[0] &= 248
    priv[31] &= 127
    priv[31] |= 64

    // Genel anahtarı hesapla — saf Go, CGo yok, wg CLI yok
    var pub [32]byte
    curve25519.ScalarBaseMult(&pub, &priv)

    privEncoded := base64.StdEncoding.EncodeToString(priv[:])
    pubEncoded  := base64.StdEncoding.EncodeToString(pub[:])

    // Yapılandırma dosyası yaz — 0600 izinleri ile
    return os.WriteFile(
        fmt.Sprintf("wg-client-%s.conf", peerName),
        []byte(conf), 0600)
}
crypto/rand + golang.org/x/crypto/curve25519 = harici araç olmadan VPN anahtar üretimi. Static binary'de her şey dahil.

fmt.Errorf("%w") — Hata Sarmalama Zinciri

📁 Tüm paketlerde kullanılan kalıp

// Her katmanda bağlam ekle
if err := o.dc.CreateNetwork(name, subnet, driver); err != nil {
    return fmt.Errorf("ağ oluştur: %w", err)
}

// Çağıran hata zincirini inceleyebilir
if errors.Is(err, os.ErrPermission) {
    // izin hatası — özel işlem
}

// Belirli bir tipe unwrap et
var netErr *net.OpError
if errors.As(err, &netErr) {
    // ağ hatası — özel işlem
}

// Go 1.20+: birden fazla hatayı birleştir
return errors.Join(err1, err2, err3)

Her hata çağrı yığınının hikayesini taşır: "provizyon başarısız: container nginx: ağa bağlan: connection refused"

sync Thread-Safe Veri Yapıları

Mutex ile IP havuzu, ikili strateji

sync.Mutex — IP Havuzunu Korumak

📁 orch/ipool/ipool.go

type IPPool struct {
    subnet string
    base   uint32
    bcast  uint32
    start  uint32
    end    uint32
    mutex  sync.Mutex              // ← tüm erişimi seri hale getirir

    avail  map[uint32]struct{}     // küçük subnet (≤1024): hazır küme
    used   map[uint32]struct{}     // büyük subnet: sadece kullanılanlar
    lazy   bool                    // host sayısı > 1024 ise true
    rand   *rand.Rand
}

func (p *IPPool) RandomIP() (string, error) {
    p.mutex.Lock()                 // kilitle
    defer p.mutex.Unlock()         // ne olursa olsun kilidi bırak

    if p.lazy {
        return p.randomIPLazy()    // büyük havuz: rastgele deneme
    }
    return p.randomIPEager()       // küçük havuz: hazır kümeden seç
}

İkili Strateji — Neden?

İkili strateji ne demek? Havuz büyüklüğüne göre farklı veri yapısı kullanmak:
Eager: ≤1024 host → tüm IP'leri map'e doldur, tahsis = sil.
Lazy: >1024 host → sadece kullanılanları yaz, rastgele üret, kontrol et.
Veri yapısını probleme uyarla.
CIDR Host Sayısı Naif map yaklaşımı Strateji
192.168.1.0/24 224 ✅ sorun yok Eager (hazır küme)
10.42.0.0/16 65.505 ⚠️ 65K giriş Lazy (deneme-yanılma)
10.0.0.0/8 16.777.185 ❌ 5 sn, ~1 GB Lazy (deneme-yanılma)

Lazy Strateji — Kod

📁 orch/ipool/ipool.go — randomIPLazy()

// Lazy strateji: rastgele başla, doğrusal tara
func (p *IPPool) randomIPLazy() (string, error) {
    total := p.end - p.start + 1
    if uint32(len(p.used)) >= total {
        return "", errors.New("IP kalmadı")
    }
    offset := uint32(p.rand.Int63n(int64(total)))
    for i := uint32(0); i < total; i++ {
        candidate := p.start + (offset+i)%total
        if _, taken := p.used[candidate]; !taken {
            p.used[candidate] = struct{}{}
            return uint32ToIP(candidate).String(), nil
        }
    }
    return "", errors.New("IP kalmadı")
}
10.0.0.0/8 havuz oluşturma: 5.1 sn → 0.004 sn (1275× hızlanma)

Eşzamanlılık Testi — 100 Goroutine

📁 orch/ipool/ipool_test.go

func TestPool_ConcurrentAccess(t *testing.T) {
    p, err := NewIPPool("172.18.5.0/24")
    if err != nil {
        t.Fatalf("havuz oluştur: %v", err)
    }

    var wg sync.WaitGroup
    errs := make(chan error, 100)

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            ip, err := p.RandomIP()    // eşzamanlı tahsis
            if err != nil {
                errs <- err
                return
            }
            if err := p.ReleaseIP(ip); err != nil {  // eşzamanlı bırakma
                errs <- err
            }
        }()
    }

    wg.Wait()
    close(errs)
    for err := range errs {
        t.Errorf("eşzamanlılık hatası: %v", err)
    }
}

go test -race ./orch/ipool/ — race detector ile doğrula.

Bonus: Goroutine-ID ile Loglama

Paralel çalışmayı gözlemlemek

runtime.Stack ile Goroutine ID Çıkarmak

📁 cmd/log_hook.go

// goroutineHook — logrus'a goroutine ID ekleyen hook
type goroutineHook struct{}

func (h *goroutineHook) Levels() []log.Level { return log.AllLevels }

func (h *goroutineHook) Fire(entry *log.Entry) error {
    entry.Data["goroutine"] = goID()
    return nil
}

func goID() string {
    var buf [64]byte
    // runtime.Stack: "goroutine 42 [running]:\n..." yazar
    n := runtime.Stack(buf[:], false)
    s := string(buf[:n])

    const prefix = "goroutine "
    s = s[len(prefix):]
    for i := 0; i < len(s); i++ {
        if s[i] == ' ' {
            if _, err := strconv.Atoi(s[:i]); err == nil {
                return s[:i]          // "42"
            }
            break
        }
    }
    return "?"
}

Log Çıktısı — Paralelliğin Kanıtı

INFO[0001] container başlatılıyor  container=nginx    goroutine=34
INFO[0001] VM tanımlanıyor         vm=demo-vm         goroutine=38
INFO[0001] container başlatılıyor  container=whoami   goroutine=36
INFO[0002] container başlatıldı    container=nginx    goroutine=34
INFO[0002] VM başlatılıyor         vm=demo-vm         goroutine=38
INFO[0003] container başlatıldı    container=whoami   goroutine=36
INFO[0004] VM başlatıldı           vm=demo-vm         goroutine=38
Farklı goroutine ID'leri = gerçek paralel çalışma. Aynı zaman damgası = eşzamanlı başlangıç.

📁 cmd/root.go — hook'u kaydet

RootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
    lvl, _ := log.ParseLevel(logLevel)
    log.SetLevel(lvl)
    log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
    log.AddHook(&goroutineHook{})     // ← her log satırına goroutine ID
    return nil
}

Demo

Hepsini bir arada görelim 🚀

Demo Akışı

# Tek komut ile derle
$ go build -o orchestrator .

# Ortamı ayağa kaldır
$ sudo ./orchestrator up -c config/example.yaml --log-level debug

# Durumu kontrol et
$ sudo ./orchestrator status

# VM'e SSH — IP otomatik algılanır
$ sudo ./orchestrator ssh demo-vm

# Her şeyi temizle
$ sudo ./orchestrator down

orchestrator up sırasında ne olur?

1. YAML yapılandırmayı ayrıştır
2. Docker bridge ağı oluştur (172.19.5.0/24)
3. IP Havuzu başlat (/8 → /30 desteği)
4. DHCP (.2) ve DNS (.3) IP'lerini rezerve et
5. DHCP + DNS container'larını başlat (paralel)
6. ┌── Container'ları başlat (paralel)        ←  goroutine
   │   ├── nginx:alpine     → 172.19.5.10
   │   └── whoami           → 172.19.5.11
   └── VM'leri başlat (paralel)               ←  goroutine
       └── demo-vm          → DHCP ile atanmış
7. WireGuard istemci yapılandırması üret       ←  crypto
8. Durum dosyası yaz (JSON)

Deklaratif Yapılandırma

network_name: demo-net
subnet: 172.19.5.0/24
network_type: bridge

containers:
  - name: web-demo
    image: nginx:alpine
    ip: 172.19.5.10

  - name: whoami
    image: containous/whoami:latest
    ip: 172.19.5.11

vms:
  - name: demo-vm
    image: ./images/debian-12.qcow2
    memory_mb: 1024
    vcpus: 2
    packages: [curl, wget, vim, htop, net-tools, nmap]

wireguard:
  enabled: true
  peer_name: demo-client
  address: 10.10.0.2/24

Çıkarımlar

Go'nun Altyapı İçin Kazandırdıkları

Go Özelliği Projede Kullanımı Alternatif Maliyet
net CIDR ayrıştırma, IP aritmetiği, broadcast Python: ipaddress + 3. parti
goroutine Container + VM paralel provizyon Thread havuzu + karmaşık senkronizasyon
channel Hata toplama, servis koordinasyonu Callback cehennem veya mutex gürültüsü
os/exec virsh, brctl, ip komutu yönetimi Shell script sarmalama / subprocess
sync Thread-safe IP havuzu, hata listesi Lock kütüphanesi + race debugging
crypto WireGuard X25519 anahtar üretimi OpenSSL bağımlılığı / CGo

Önemli Tasarım Kararları

  • Zarif geri çekilme — KVM yoksa QEMU, cloud-init yoksa CD-ROM'u atla
  • uint32 ile düşün — IPv4 aritmetiği tam sayı karşılaştırmasına dönüşür
  • İkili strateji — veri yapısını girdi boyutuna uyarla (eager vs. lazy)
  • Hata zinciri%w ile her katmanda bağlam ekle
  • Fan-out + channel — Go'nun eşzamanlılık ilkelleri doğal olarak birleşir
  • Eşzamanlılığı test et — 100 goroutine + race detector = güven

Sonraki Adımlar

  • Context tabanlı iptal ve zarif kapatma
  • gRPC API ile uzaktan orkestrasyon
  • Prometheus metrikleri (container sayısı, IP kullanımı, …)
  • VM snapshot ve geri yükleme
  • WireGuard mesh ile çoklu sunucu desteği

Sorular?

Soru & Cevap



Ahmet Türkmen
linkedin.com/in/mrturkmen
github.com/mrtrkmn
Systems Development Engineer — AWS


Teşekkürler! 🙏

xkcd #1988 — Containers

xkcd.com/1988 — "Containers"