Container · VM · Ağ · VPN — tek bir binary
Ahmet Türkmen
Systems Development Engineer — AWS
Gophers İstanbul · 2026
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
Orkestrasyon yazılımı için Go'yu seçmek
$ 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ı.
Bulut altyapısının fiili dili
| 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 |
virsh domain'lerini doğrudan yönetemez# 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 .
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
Her kutu bağımsız bir Go paketi — açık bağımlılıklar, kolay test.
IPv4 aritmetiği, CIDR ayrıştırma, broadcast hesaplama
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şı.
📁 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:172.19.5.0),
2. Ağ bilgisi — ağ adresi + maske (172.19.5.0/24),
3. Hata (geçersiz CIDR ise).📁 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)
}
10.42.0.0 → 175_046_656 |
10.42.255.255 → 175_112_191if yeter.
📁 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)
172.19.5.255
2. Maske → 255.255.255.0
3. Sabit IP: .2 = DHCP, .3 = DNS
4. Aralık: .4 – .254 istemcilerenet + encoding/binary ile — harici kütüphane sıfır.
📁 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.
Container + VM'leri eşzamanlı provizyon etmek
📁 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
📁 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ç
}
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ış).go func(container ContainerCfg) { ... }(c)Not: Go 1.22+ bu sorunu otomatik çözer, ancak eski sürümlerle uyumluluk için bu kalıp hâlâ önerilir.
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ı
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
Goroutine'ler arası hata toplama ve senkronizasyon
📁 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
}
make(chan error, N) ile N goroutine hata
gönderebilir
ve hiçbiri bloklanmaz. Unbuffered olsaydı her gönderim bir alıcı beklerdi.
📁 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.
📁 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.
virsh, dosya sistemi, kriptografi — hepsi stdlib'de
| 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 |
📁 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.
📁 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
}
/dev/kvm yoktur — aracınız çökmek yerine
zarif bir şekilde geri çekilmelidir.
📁 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.
📁 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"
Mutex ile IP havuzu, ikili strateji
📁 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ç
}
map'e doldur, tahsis = sil.| 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) |
📁 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ı")
}
📁 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.
Paralel çalışmayı gözlemlemek
📁 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 "?"
}
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
📁 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
}
Hepsini bir arada görelim 🚀
# 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)
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
| 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 |
%w ile her katmanda bağlam ekleSoru & Cevap
Ahmet Türkmen
linkedin.com/in/mrturkmen
github.com/mrtrkmn
Systems Development Engineer — AWS
Teşekkürler! 🙏
xkcd.com/1988 — "Containers"