Go Uygulamasını Systemd Servis Olarak Çalıştırma

Üretim ortamında bir Go uygulaması yazdınız, testler geçti, binary hazır. Peki bu uygulamayı sunucuda kalıcı olarak nasıl çalıştıracaksınız? Ekranı kapatınca process ölüyor, sunucu yeniden başlayınca uygulama ayağa kalkmıyor. İşte tam bu noktada systemd devreye giriyor. Systemd ile Go uygulamanızı gerçek bir sistem servisi haline getirip, otomatik başlatma, log yönetimi ve crash recovery gibi özellikleri kazanabilirsiniz.

Go Binary’sini Hazırlamak

Önce sağlam bir binary oluşturmakla başlayalım. Go’nun güzel yanlarından biri statik binary üretebilmesi. Bu sayede hedef sunucuda Go runtime kurulu olmak zorunda değil.

# Üretim için optimize binary derle
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o myapp ./cmd/myapp

# Binary boyutunu kontrol et
ls -lh myapp

# Binary'nin gerçekten statik olduğunu doğrula
file myapp
ldd myapp

CGO_ENABLED=0: C bağımlılıklarını devre dışı bırakır, tam statik binary üretir.

-ldflags=”-w -s”: Debug sembollerini ve DWARF bilgilerini çıkarır, binary boyutunu küçültür.

GOOS=linux GOARCH=amd64: Çapraz derleme için hedef işletim sistemi ve mimariyi belirtir. Geliştirme makineniz macOS veya Windows olsa bile Linux binary üretirsiniz.

Binary’yi sunucuya kopyalayın ve uygun bir dizine yerleştirin:

# Sunucuda dizin yapısını oluştur
sudo mkdir -p /opt/myapp/bin
sudo mkdir -p /opt/myapp/config
sudo mkdir -p /opt/myapp/logs

# Binary'yi kopyala
sudo cp myapp /opt/myapp/bin/myapp
sudo chmod +x /opt/myapp/bin/myapp

# Config dosyasını kopyala
sudo cp config.yaml /opt/myapp/config/config.yaml

Dedicated Kullanıcı Oluşturmak

Uygulamayı root ile çalıştırmak büyük bir güvenlik riski. Her production servis için ayrı bir kullanıcı açın:

# Sistem kullanıcısı oluştur (login shell yok, home dizini yok)
sudo useradd --system --no-create-home --shell /bin/false myapp

# Dizin sahipliğini ayarla
sudo chown -R myapp:myapp /opt/myapp

# Log dizini için özel izinler
sudo chmod 750 /opt/myapp/logs

# Kullanıcının oluşturulduğunu doğrula
id myapp

–system: UID aralığı olarak düşük numaralı sistem UID’lerini kullanır.

–no-create-home: Home dizini oluşturmaz, gereksiz risk azaltır.

–shell /bin/false: Bu kullanıcıyla SSH veya terminal girişi yapılamaz.

Temel Systemd Unit Dosyası

Şimdi asıl konuya gelelim. /etc/systemd/system/myapp.service dosyasını oluşturun:

sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=MyApp Go Uygulamasi
After=network.target
Wants=network-online.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/myapp --config /opt/myapp/config/config.yaml
Restart=on-failure
RestartSec=5s

StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

[Install]
WantedBy=multi-user.target

Dosyayı kaydettikten sonra servisi aktifleştirin:

# Systemd'yi yeni unit dosyasından haberdar et
sudo systemctl daemon-reload

# Servisi başlat
sudo systemctl start myapp

# Otomatik başlatmayı etkinleştir
sudo systemctl enable myapp

# Durumu kontrol et
sudo systemctl status myapp

Kapsamlı ve Güvenli Unit Dosyası

Temel unit dosyası çalışır ama üretim ortamı için daha fazlasına ihtiyaç var. Güvenlik kısıtlamaları, environment değişkenleri ve kaynak limitleri ekleyelim:

[Unit]
Description=MyApp Go Uygulamasi
Documentation=https://github.com/yourorg/myapp
After=network.target network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp

# Calistirilacak komut
ExecStart=/opt/myapp/bin/myapp --config /opt/myapp/config/config.yaml
ExecReload=/bin/kill -HUP $MAINPID

# Environment degiskenleri
Environment=APP_ENV=production
Environment=LOG_LEVEL=info
EnvironmentFile=-/opt/myapp/config/env

# Restart politikasi
Restart=always
RestartSec=10s
StartLimitIntervalSec=60s
StartLimitBurst=3

# Kaynak limitleri
LimitNOFILE=65535
LimitNPROC=4096

# Guvenlik kisitlamalari
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp/logs /opt/myapp/data
CapabilityBoundingSet=
AmbientCapabilities=

# Log ayarlari
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

# Timeout ayarlari
TimeoutStartSec=30s
TimeoutStopSec=30s

[Install]
WantedBy=multi-user.target

Bu dosyadaki kritik parametreleri açıklayalım:

After=postgresql.service: Servis başlamadan önce PostgreSQL’in hazır olmasını bekler.

Requires=postgresql.service: PostgreSQL çökerse bu servis de durur.

ExecReload=/bin/kill -HUP $MAINPID: Go uygulamanız SIGHUP sinyalini dinliyorsa config reload yapabilirsiniz.

Restart=always: Uygulama herhangi bir nedenle çökerse otomatik yeniden başlatır.

StartLimitBurst=3: 60 saniye içinde 3 kezden fazla çökerse systemd yeniden başlatmayı durdurur, böylece crash loop’a girmez.

NoNewPrivileges=true: Process’in yeni ayrıcalıklar kazanmasını engeller.

PrivateTmp=true: /tmp dizinini izole eder, diğer processlerden gizler.

ProtectSystem=strict: Dosya sistemini read-only yapar, sadece ReadWritePaths’te belirtilen yerlere yazabilir.

LimitNOFILE=65535: Açık dosya/bağlantı limitini artırır. Yüksek trafikli HTTP serverlarda çok önemli.

Environment Dosyası ile Gizli Bilgi Yönetimi

Şifreler ve API anahtarlarını unit dosyasına yazmayın. Ayrı bir env dosyası kullanın:

sudo nano /opt/myapp/config/env
DATABASE_URL=postgres://user:secretpassword@localhost:5432/myappdb
REDIS_URL=redis://localhost:6379/0
JWT_SECRET=supersecretjwtkey123
API_KEY=external-api-key-here
SMTP_PASSWORD=emailpassword
# Dosya izinlerini kısıtla, sadece myapp kullanıcısı okuyabilsin
sudo chown myapp:myapp /opt/myapp/config/env
sudo chmod 600 /opt/myapp/config/env

# Root dışında kimse okuyamasın diye kontrol et
sudo -u www-data cat /opt/myapp/config/env  # Permission denied vermeli

Unit dosyasındaki EnvironmentFile=-/opt/myapp/config/env satırındaki - işaretine dikkat edin. Bu işaret dosya yoksa hata vermemesini sağlar, servis yine de başlar.

Go Uygulamanızda Graceful Shutdown

Systemd servisi olarak çalışan Go uygulamanızın SIGTERM sinyalini düzgün yakalaması gerekiyor. Aksi halde systemd TimeoutStopSec dolduğunda SIGKILL gönderir ve açık bağlantılar, yarım kalan işlemler sorun çıkarır:

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    srv := &http.Server{
        Addr:    ":8080",
        Handler: setupRoutes(),
    }

    // Serveri goroutine icinde baslat
    go func() {
        log.Println("Server :8080 portunda dinliyor")
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server baslatma hatasi: %v", err)
        }
    }()

    // OS sinyallerini yakala
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

    // Sinyal gelene kadar bekle
    sig := <-quit
    log.Printf("Sinyal alindi: %v, graceful shutdown basliyor...", sig)

    // 30 saniye timeout ile graceful shutdown
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("Zorla kapama gerekti: %v", err)
    }

    log.Println("Server temiz sekilde kapandi")
}

Bu yapı sayesinde systemctl stop myapp veya systemctl restart myapp komutlarında uygulama mevcut requestleri tamamlayıp düzgünce kapanır.

Log Yönetimi

Systemd journald ile entegre log yönetimi oldukça güçlü:

# Servis loglarını canlı izle
sudo journalctl -u myapp -f

# Son 100 satır log
sudo journalctl -u myapp -n 100

# Bugünkü loglar
sudo journalctl -u myapp --since today

# Belirli zaman aralığı
sudo journalctl -u myapp --since "2024-01-15 10:00:00" --until "2024-01-15 11:00:00"

# Sadece hata logları
sudo journalctl -u myapp -p err

# JSON formatında çıktı (log analizi için)
sudo journalctl -u myapp -o json | jq .

Eğer uygulamanın log dosyasına da yazmasını istiyorsanız, systemd ile birlikte çalışan bir yapı kurabilirsiniz:

# Log rotation için logrotate konfigürasyonu
sudo nano /etc/logrotate.d/myapp
/opt/myapp/logs/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 myapp myapp
    postrotate
        systemctl kill --kill-who=main --signal=USR1 myapp
    endscript
}

Servis Yönetimi ve Sorun Giderme

Günlük operasyonlarda kullanacağınız komutlar:

# Servis durumu (detaylı)
sudo systemctl status myapp -l

# Başlat / Durdur / Yeniden başlat
sudo systemctl start myapp
sudo systemctl stop myapp
sudo systemctl restart myapp

# Config reload (ExecReload tanımlıysa)
sudo systemctl reload myapp

# Otomatik başlatmayı devre dışı bırak
sudo systemctl disable myapp

# Servisin aktif olup olmadığını kontrol et (script için)
systemctl is-active myapp && echo "Calisiyor" || echo "Durdu"

# Servis dosyasını değiştirdikten sonra
sudo systemctl daemon-reload
sudo systemctl restart myapp

# Tüm failed servisleri listele
systemctl --failed

# Servis bağımlılıklarını görüntüle
systemctl list-dependencies myapp

Bir servis başlamıyorsa ilk bakılacak yer:

# Detaylı başlatma hatası için
sudo journalctl -u myapp -n 50 --no-pager

# Systemd'nin servis hakkındaki son olayları
sudo systemctl status myapp --output=verbose

# Binary'nin çalışıp çalışmadığını manuel test et
sudo -u myapp /opt/myapp/bin/myapp --config /opt/myapp/config/config.yaml

# Port çakışması kontrolü
sudo ss -tlnp | grep 8080

Birden Fazla Instance Çalıştırmak

Aynı uygulamanın birden fazla instance’ını çalıştırmak için template unit dosyaları kullanabilirsiniz. Bu özellikle farklı portlarda veya farklı config’lerle aynı binary’yi çalıştırmanız gerektiğinde işe yarar:

sudo nano /etc/systemd/system/[email protected]
[Unit]
Description=MyApp Go Uygulamasi - Instance %i
After=network.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/myapp --config /opt/myapp/config/config-%i.yaml
Restart=on-failure
RestartSec=5s
Environment=INSTANCE=%i
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp-%i

[Install]
WantedBy=multi-user.target
# Farkli instance'ları baslat
sudo systemctl start myapp@production
sudo systemctl start myapp@staging
sudo systemctl enable myapp@production

# Instance loglarını ayrı takip et
sudo journalctl -u myapp@production -f

Health Check ve Watchdog Entegrasyonu

Systemd’nin watchdog özelliği, uygulamanızın gerçekten sağlıklı çalışıp çalışmadığını periyodik olarak kontrol eder:

[Service]
# ... diger ayarlar ...
WatchdogSec=30s
NotifyAccess=main
Type=notify

Go uygulamanızda watchdog bildirimlerini göndermek için:

package main

import (
    "log"
    "net"
    "os"
    "time"
)

// Systemd'ye hazir bildirimi gonder
func sdNotify(state string) error {
    socketAddr := os.Getenv("NOTIFY_SOCKET")
    if socketAddr == "" {
        return nil
    }

    conn, err := net.Dial("unixgram", socketAddr)
    if err != nil {
        return err
    }
    defer conn.Close()

    _, err = conn.Write([]byte(state))
    return err
}

func main() {
    // Uygulamayi baslat
    if err := initApp(); err != nil {
        log.Fatal(err)
    }

    // Systemd'ye hazir oldugunu bildir
    if err := sdNotify("READY=1"); err != nil {
        log.Printf("Systemd bildirim hatasi: %v", err)
    }

    // Watchdog dongusu
    go func() {
        interval := 15 * time.Second
        ticker := time.NewTicker(interval)
        defer ticker.Stop()

        for range ticker.C {
            if isHealthy() {
                sdNotify("WATCHDOG=1")
            }
        }
    }()

    // Ana uygulama dongusu
    runApp()
}

func isHealthy() bool {
    // Veritabani baglantisi, kritik servisler vs kontrol et
    return true
}

Bu yapıyla uygulama 30 saniye boyunca watchdog sinyali gönderemezse systemd servisi otomatik yeniden başlatır. Gerçek bir hayatta kurtarma mekanizması.

Deployment Workflow

Binary güncelleme sürecini de otomatize etmek için basit bir deploy scripti:

#!/bin/bash
set -e

BINARY_PATH="/opt/myapp/bin/myapp"
NEW_BINARY="$1"
SERVICE_NAME="myapp"

if [ -z "$NEW_BINARY" ]; then
    echo "Kullanim: $0 <yeni-binary-yolu>"
    exit 1
fi

echo "Yeni binary kontrol ediliyor..."
if ! file "$NEW_BINARY" | grep -q "ELF 64-bit"; then
    echo "Hata: Gecersiz binary dosyasi"
    exit 1
fi

echo "Eski binary yedekleniyor..."
sudo cp "$BINARY_PATH" "${BINARY_PATH}.backup"

echo "Yeni binary kopyalaniyor..."
sudo cp "$NEW_BINARY" "$BINARY_PATH"
sudo chmod +x "$BINARY_PATH"
sudo chown myapp:myapp "$BINARY_PATH"

echo "Servis yeniden baslatiliyor..."
sudo systemctl restart "$SERVICE_NAME"

echo "Servis durumu kontrol ediliyor..."
sleep 3
if systemctl is-active --quiet "$SERVICE_NAME"; then
    echo "Deploy basarili! Servis calisiyor."
else
    echo "Hata! Servis baslamadi, geri donuluyor..."
    sudo cp "${BINARY_PATH}.backup" "$BINARY_PATH"
    sudo systemctl restart "$SERVICE_NAME"
    exit 1
fi
sudo chmod +x /usr/local/bin/deploy-myapp

Sonuç

Go uygulamanızı systemd servisi olarak çalıştırmak birkaç adımlık bir süreç olsa da doğru yapılandırıldığında size çok güçlü bir altyapı sunuyor. Özet olarak dikkat etmeniz gereken noktalar şunlar:

  • Her zaman dedicated sistem kullanıcısıyla çalıştırın, root’tan kaçının.
  • ProtectSystem, NoNewPrivileges ve PrivateTmp gibi güvenlik direktiflerini mutlaka ekleyin.
  • Gizli bilgileri unit dosyasına değil, izinleri kısıtlanmış env dosyasına yazın.
  • Go uygulamanızda SIGTERM sinyalini yakalayan graceful shutdown kodunu mutlaka implement edin.
  • StartLimitBurst ile crash loop senaryosuna karşı kendinizi koruyun.
  • Watchdog entegrasyonu ile sadece process ayakta olması değil, gerçekten sağlıklı çalışması durumunu da izleyin.

Bu yapıyı bir kez kurduğunuzda sunucu restartları, crash’ler ve deployment’lar çok daha az stresli hale geliyor. Systemd’nin log yönetimi, bağımlılık sistemi ve kaynak limitlerinden tam olarak yararlandığınızda Go uygulamanız gerçek anlamda production-ready bir servis haline geliyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir