Клієнт-серверна архітектура

Моніторинг, безпека та контейнеризація застосунків

Комплексне вивчення спостережуваності Go-застосунків через структуроване логування (log/slog), збір метрик для Prometheus, захист серверних інтерфейсів від типових вразливостей та створення мінімальних безпечних Docker-образів із багатоетапним складанням

Моніторинг, безпека та контейнеризація застосунків

🎯 Мета лекції

  • Опанувати три стовпи спостережуваності (Observability): логування, метрики та трасування.
  • Навчитися використовувати стандартний пакет log/slog для структурованого логування у Go.
  • Інтегрувати систему збору метрик Prometheus для моніторингу продуктивності застосунків.
  • Реалізувати Health Check ендпоінти для перевірки стану сервісів.
  • Зрозуміти типові вразливості серверних застосунків (OWASP Top 10) та методи захисту.
  • Застосувати криптографічне хешування паролів через bcrypt та захисні HTTP-заголовки.
  • Опанувати контейнеризацію Go-застосунків через Docker із багатоетапним складанням (Multi-stage Build).
  • Створити мінімальні безпечні образи на базі scratch або alpine з non-root користувачем.

🔑 Ключові терміни

  • Observability (спостережуваність): можливість зрозуміти внутрішній стан системи на основі зовнішніх сигналів (логів, метрик, трас).
  • Structured Logging (структуроване логування): формат логів у вигляді пар ключ-значення (JSON, logfmt) замість вільного тексту.
  • Metrics (метрики): числові показники роботи системи (RPS, латентність, використання пам'яті), що збираються з часом.
  • Health Check: спеціальний ендпоінт для перевірки готовності сервісу обслуговувати запити (liveness/readiness probes).
  • Prometheus: система моніторингу з моделлю pull (сервер періодично запитує метрики у застосунків).
  • bcrypt: алгоритм хешування паролів з автоматичним солінням та адаптивною складністю.
  • Security Headers: HTTP-заголовки для захисту від XSS, Clickjacking, MIME-sniffing та інших атак.
  • Docker Multi-stage Build: техніка створення мінімальних образів через використання декількох етапів збірки.
  • Distroless / Scratch Image: базові образи без ОС та утиліт, що містять лише мінімум для виконання застосунку.

Три стовпи спостережуваності систем

У сучасній розподіленій архітектурі мікросервісів традиційного моніторингу (перевірка, чи працює процес) недостатньо для діагностики складних проблем. Концепція Observability (спостережуваність) визначає три фундаментальні сигнали, що дозволяють зрозуміти поведінку системи:

1. Логування (Logging)

Логи — це часово впорядковані записи дискретних подій, що відбулися у системі. Кожен запис містить часову мітку, рівень важливості (INFO, WARN, ERROR) та контекстну інформацію про подію.

Типи логів:

  • Транзакційні логи: запис кожного HTTP-запиту, SQL-запиту, виклику зовнішнього API.
  • Логи помилок: винятки, збої, відхилені з'єднання, таймаути.
  • Аудит-логи: критичні дії користувачів (вхід у систему, зміна налаштувань, видалення даних).
  • Діагностичні логи: розширена інформація для налагодження (значення змінних, стани об'єктів).

Проблема традиційного логування:

Класичний підхід із рядковим форматуванням (log.Printf("User %s logged in from %s", username, ip)) створює неструктуровані текстові логи, які важко аналізувати автоматично. Пошук усіх входів конкретного користувача вимагає регулярних виразів та складного парсингу.

Структуроване логування вирішує цю проблему, записуючи логи у форматі пар ключ-значення (JSON або logfmt):

{
  "timestamp": "2026-09-02T14:30:00Z",
  "level": "INFO",
  "message": "User logged in",
  "user_id": "12345",
  "username": "john.doe",
  "ip_address": "203.0.113.42",
  "user_agent": "Mozilla/5.0..."
}

Такий формат дозволяє легко фільтрувати, агрегувати та візуалізувати логи у системах централізованого збору (ELK Stack, Grafana Loki, Datadog).

2. Метрики (Metrics)

Метрики — це числові показники, що вимірюються з часом та агрегуються для виявлення трендів, аномалій та побудови дашбордів. На відміну від логів, метрики мають фіксований розмір у пам'яті та дозволяють ефективно зберігати історичні дані.

Основні типи метрик:

  • Counter (лічильник): монотонно зростаюче значення (кількість оброблених HTTP-запитів, відправлених повідомлень). Ніколи не зменшується, скидається при рестарті.
  • Gauge (датчик): значення, що може зростати та зменшуватися (поточна кількість активних з'єднань, використання пам'яті, температура CPU).
  • Histogram (гістограма): розподіл значень по корзинах (buckets). Використовується для вимірювання латентності, розмірів запитів/відповідей. Дозволяє обчислювати перцентилі (p50, p95, p99).
  • Summary: аналогічна гістограмі, але обчислює перцентилі на стороні клієнта (точніші, але більш ресурсоємні).

Приклади метрик для вебсервера:

  • http_requests_total (Counter): загальна кількість HTTP-запитів з мітками method, path, status_code.
  • http_request_duration_seconds (Histogram): тривалість обробки запитів.
  • active_connections (Gauge): поточна кількість активних TCP-з'єднань.
  • database_connection_pool_size (Gauge): розмір пулу з'єднань до БД.

3. Трасування (Tracing)

Розподілене трасування (Distributed Tracing) відслідковує шлях запиту через всі мікросервіси розподіленої системи. Кожен запит отримує унікальний ідентифікатор (trace_id), що пересилається між сервісами через HTTP-заголовки. Кожна операція у ланцюжку створює span (відрізок) з інформацією про час початку, завершення та метаданими.

Приклад трасування запиту до системи електронної комерції:

Trace ID: abc123
├─ Span: API Gateway (20ms)
│  ├─ Span: Auth Service (5ms)
│  ├─ Span: Product Service (80ms)
│  │  └─ Span: PostgreSQL Query (60ms)
│  └─ Span: Inventory Service (30ms)
│     └─ Span: Redis Cache (2ms)
└─ Total: 135ms

Трасування дозволяє виявити вузькі місця (у прикладі — запит до PostgreSQL займає 60 мс із 135 мс загального часу) та зрозуміти залежності між сервісами.

Найпопулярніші системи розподіленого трасування: Jaeger (від Uber, CNCF), Zipkin (від Twitter), Tempo (від Grafana Labs). Стандартизація відбувається через специфікацію OpenTelemetry, що об'єднує API для метрик, логування та трасування.

Структуроване логування у Go: Пакет log/slog

Починаючи з Go 1.21, стандартна бібліотека включає пакет log/slog (structured logging) — сучасну систему структурованого логування, що замінює застарілий log. Пакет надає API для записування логів у JSON-форматі, підтримує контекстні атрибути та інтегрується з системами централізованого збору.

Основи використання slog

Створення базового логера та запис подій:

package main

import (
    "log/slog"
    "os"
)

func main() {
    // Створення JSON-логера, що пише у stdout
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    // Встановлення як глобального логера за замовчуванням
    slog.SetDefault(logger)
    
    // Логування повідомлень різних рівнів
    slog.Info("Server starting", "port", 8080, "environment", "production")
    slog.Warn("High memory usage detected", "usage_mb", 1024, "threshold_mb", 900)
    slog.Error("Database connection failed", "error", "connection timeout", "retry_count", 3)
    
    // Логування з додатковими атрибутами
    userID := "user_12345"
    slog.Info("User action performed",
        "user_id", userID,
        "action", "purchase",
        "amount", 99.99,
        "currency", "USD",
    )
}

Вихід у форматі JSON:

{"time":"2026-09-02T14:30:00.123456Z","level":"INFO","msg":"Server starting","port":8080,"environment":"production"}
{"time":"2026-09-02T14:30:01.234567Z","level":"WARN","msg":"High memory usage detected","usage_mb":1024,"threshold_mb":900}
{"time":"2026-09-02T14:30:02.345678Z","level":"ERROR","msg":"Database connection failed","error":"connection timeout","retry_count":3}
{"time":"2026-09-02T14:30:03.456789Z","level":"INFO","msg":"User action performed","user_id":"user_12345","action":"purchase","amount":99.99,"currency":"USD"}

Рівні логування та налаштування

Пакет slog підтримує чотири стандартні рівні логування (від найменш до найбільш критичних):

  • DEBUG (-4): детальна діагностична інформація для розробників (значення змінних, хід виконання).
  • INFO (0, за замовчуванням): інформаційні повідомлення про нормальну роботу системи.
  • WARN (4): попередження про потенційні проблеми, що не призводять до збоїв.
  • ERROR (8): помилки, що вимагають уваги (збій операції, недоступність залежності).

Налаштування мінімального рівня логування:

// Створення обробника з мінімальним рівнем WARNING
handlerOptions := &slog.HandlerOptions{
    Level: slog.LevelWarn, // Ігноруються DEBUG та INFO
}
handler := slog.NewJSONHandler(os.Stdout, handlerOptions)
logger := slog.New(handler)

logger.Debug("This will not be logged")  // Пропускається
logger.Info("This will not be logged")   // Пропускається
logger.Warn("This will be logged")       // Виводиться
logger.Error("This will be logged")      // Виводиться

Контекстні атрибути та групування

Для додавання постійних атрибутів до всіх логів конкретного логера використовується метод With():

// Створення логера з постійними атрибутами
serviceLogger := logger.With(
    "service", "payment-processor",
    "version", "v2.3.1",
    "instance_id", "i-abc123",
)

// Усі подальші логи автоматично містять ці атрибути
serviceLogger.Info("Transaction started", "transaction_id", "tx_789")
// {"time":"...","level":"INFO","msg":"Transaction started","service":"payment-processor","version":"v2.3.1","instance_id":"i-abc123","transaction_id":"tx_789"}

// Групування атрибутів для ієрархічної структури
serviceLogger.Info("User profile updated",
    slog.Group("user",
        "id", "12345",
        "username", "john.doe",
        "email", "john@example.com",
    ),
    slog.Group("changes",
        "field", "phone_number",
        "old_value", "+380501234567",
        "new_value", "+380509876543",
    ),
)

Результат:

{
  "time": "2026-09-02T14:30:00Z",
  "level": "INFO",
  "msg": "User profile updated",
  "service": "payment-processor",
  "version": "v2.3.1",
  "instance_id": "i-abc123",
  "user": {
    "id": "12345",
    "username": "john.doe",
    "email": "john@example.com"
  },
  "changes": {
    "field": "phone_number",
    "old_value": "+380501234567",
    "new_value": "+380509876543"
  }
}

Логування HTTP-запитів через Middleware

Інтеграція slog у вебсервер для автоматичного логування всіх HTTP-запитів:

package main

import (
    "log/slog"
    "net/http"
    "time"
)

// LoggingMiddleware логує кожен HTTP-запит із метриками продуктивності
func LoggingMiddleware(logger *slog.Logger, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Обгортка ResponseWriter для захоплення статус-коду
        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        
        // Виконання наступного обробника
        next.ServeHTTP(wrapped, r)
        
        // Обчислення тривалості обробки
        duration := time.Since(start)
        
        // Логування після завершення запиту
        logger.Info("HTTP request completed",
            "method", r.Method,
            "path", r.URL.Path,
            "status", wrapped.statusCode,
            "duration_ms", duration.Milliseconds(),
            "remote_addr", r.RemoteAddr,
            "user_agent", r.UserAgent(),
        )
    })
}

// responseWriter обгортає http.ResponseWriter для захоплення статус-коду
type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    mux := http.NewServeMux()
    mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"status":"healthy"}`))
    })
    
    // Застосування middleware до всіх маршрутів
    handler := LoggingMiddleware(logger, mux)
    
    slog.Info("Starting server", "port", 8080)
    http.ListenAndServe(":8080", handler)
}
Для продакшн-середовищ розгляньте використання бібліотеки github.com/rs/zerolog або go.uber.org/zap — вони забезпечують вищу продуктивність (до 10× швидше за slog) через техніки zero-allocation та пулінг об'єктів. Проте slog є чудовим вибором для більшості застосунків завдяки простоті API та статусу стандартної бібліотеки.

Збір метрик для Prometheus

Prometheus — це система моніторингу з відкритим кодом, розроблена у SoundCloud та прийнята як другий проєкт Cloud Native Computing Foundation (після Kubernetes). Prometheus використовує модель pull: сервер періодично (зазвичай кожні 15–30 секунд) надсилає HTTP-запити до застосунків для збору метрик.

Архітектура інтеграції Prometheus

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

component "Go Application\n:8080" as App #DBEAFE
component "Metrics Endpoint\n/metrics" as Metrics #DCFCE7
database "Prometheus Server\n:9090" as Prom #FEF3C7
component "Grafana\n:3000" as Grafana #E2E8F0
actor "Developer/Ops" as User #F1F5F9

App -down-> Metrics : Експонує метрики
Prom -up-> Metrics : HTTP GET /metrics\n(кожні 15 секунд)
Prom -right-> Grafana : PromQL запити
User -up-> Grafana : Перегляд дашбордів

note right of Metrics
  Формат Prometheus:
  # HELP http_requests_total Total HTTP requests
  # TYPE http_requests_total counter
  http_requests_total{method="GET",path="/api/users"} 1234
end note

@enduml

Інтеграція бібліотеки prometheus/client_golang

Офіційний Go-клієнт Prometheus надає API для створення та реєстрації метрик:

package main

import (
    "net/http"
    "time"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    // Counter: загальна кількість HTTP-запитів
    httpRequestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "path", "status"}, // Мітки для фільтрації
    )
    
    // Histogram: розподіл тривалості запитів
    httpRequestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds",
            // Buckets визначають межі корзин для групування вимірювань
            // Вибір buckets критично важливий для точності перцентилів
            Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0},
        },
        []string{"method", "path"},
    )
    
    // Gauge: поточна кількість активних з'єднань
    activeConnections = prometheus.NewGauge(
        prometheus.GaugeOpts{
            Name: "active_connections",
            Help: "Number of active connections",
        },
    )
)

func init() {
    // Реєстрація метрик у глобальному реєстрі
    prometheus.MustRegister(httpRequestsTotal)
    prometheus.MustRegister(httpRequestDuration)
    prometheus.MustRegister(activeConnections)
}

// MetricsMiddleware збирає метрики для кожного HTTP-запиту
func MetricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        activeConnections.Inc() // Збільшення лічильника активних з'єднань
        defer activeConnections.Dec() // Зменшення після завершення
        
        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        next.ServeHTTP(wrapped, r)
        
        duration := time.Since(start).Seconds()
        
        // Оновлення метрик
        httpRequestsTotal.WithLabelValues(
            r.Method,
            r.URL.Path,
            http.StatusText(wrapped.statusCode),
        ).Inc()
        
        httpRequestDuration.WithLabelValues(
            r.Method,
            r.URL.Path,
        ).Observe(duration)
    })
}

func main() {
    mux := http.NewServeMux()
    
    // Ендпоінт для збору метрик Prometheus
    mux.Handle("/metrics", promhttp.Handler())
    
    // Бізнес-логіка
    mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(50 * time.Millisecond) // Симуляція роботи
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"users": []}`))
    })
    
    handler := MetricsMiddleware(mux)
    
    http.ListenAndServe(":8080", handler)
}

Приклад виходу ендпоінту /metrics:

# HELP http_requests_total Total number of HTTP requests
# TYPE http_requests_total counter
http_requests_total{method="GET",path="/api/users",status="OK"} 1234
http_requests_total{method="POST",path="/api/users",status="Created"} 567

# HELP http_request_duration_seconds HTTP request duration in seconds
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{method="GET",path="/api/users",le="0.005"} 102
http_request_duration_seconds_bucket{method="GET",path="/api/users",le="0.01"} 450
http_request_duration_seconds_bucket{method="GET",path="/api/users",le="0.05"} 1150
http_request_duration_seconds_bucket{method="GET",path="/api/users",le="+Inf"} 1234
http_request_duration_seconds_sum{method="GET",path="/api/users"} 45.678
http_request_duration_seconds_count{method="GET",path="/api/users"} 1234

# HELP active_connections Number of active connections
# TYPE active_connections gauge
active_connections 12

Вибір правильних Buckets для Histogram

Ефективність Histogram залежить від правильного вибору меж корзин (buckets). Buckets визначають діапазони, у які потраплятимуть виміряні значення.

Принципи вибору buckets:

  1. Покриття очікуваного діапазону: Мінімальний bucket має бути нижчим за найшвидший запит, максимальний — вищим за найповільніший. Значення поза діапазоном потрапляють у bucket +Inf, але перцентилі будуть неточними.
  2. Логарифмічний розподіл: Для латентності використовуйте експоненційний крок (×2, ×5, ×10), оскільки різниця між 1 мс та 10 мс є критичнішою, ніж між 1 с та 1.1 с.
  3. Баланс між точністю та пам'яттю: Кожен bucket створює окрему часову серію в Prometheus. 10 buckets × 5 міток (method, path, status) = 50 часових серій на одну метрику. Надмірна деталізація споживає пам'ять.

Приклади для різних сценаріїв:

// Швидкі API (кешовані запити, in-memory операції)
Buckets: prometheus.LinearBuckets(0.001, 0.001, 10) // 1ms, 2ms, 3ms, ..., 10ms

// Типові веб-запити (БД запити, зовнішні API)
Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0}

// Повільні операції (генерація звітів, обробка файлів)
Buckets: prometheus.ExponentialBuckets(0.1, 2, 10) // 0.1, 0.2, 0.4, 0.8, 1.6, 3.2, ...

// Універсальний (DefBuckets у Prometheus)
Buckets: prometheus.DefBuckets // 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10

Аналіз похибки перцентилів:

Prometheus обчислює перцентилі через лінійну інтерполяцію між buckets. Якщо p95 потрапляє між buckets 0.1 та 0.5, результат буде наближеним. Чим більше buckets у критичному діапазоні, тим точніший результат.

Функція histogram_quantile() у PromQL використовує лінійну інтерполяцію між buckets, припускаючи рівномірний розподіл значень усередині кожного bucket. Для реальних метрик із довгим хвостом (heavy-tailed distribution) це може призводити до похибки 10–20%. Альтернатива — Summary з точними перцентилями, але вона не підтримує агрегацію між інстансами.

Налаштування Prometheus Server

Конфігураційний файл prometheus.yml для scraping метрик:

global:
  scrape_interval: 15s     # Інтервал збору метрик
  evaluation_interval: 15s # Інтервал оцінки правил алертів

scrape_configs:
  - job_name: 'go-app'
    static_configs:
      - targets: ['localhost:8080']
        labels:
          environment: 'production'
          region: 'eu-west'

Запуск Prometheus через Docker:

docker run -d \
  --name prometheus \
  -p 9090:9090 \
  -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \
  prom/prometheus

Типові PromQL-запити для аналізу

Prometheus Query Language (PromQL) дозволяє виконувати складні запити та агрегації метрик:

1. Швидкість запитів (RPS - Requests Per Second):

rate(http_requests_total[5m])

2. 95-й перцентиль латентності за останні 5 хвилин:

histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

3. Відсоток помилок (HTTP 5xx) серед усіх запитів:

sum(rate(http_requests_total{status=~"5.."}[5m])) 
/ 
sum(rate(http_requests_total[5m])) * 100

4. Топ-5 найповільніших ендпоінтів:

topk(5, 
  histogram_quantile(0.99, 
    rate(http_request_duration_seconds_bucket[10m])
  )
)
У продакшн-середовищах Prometheus інтегрується з системою візуалізації Grafana для створення дашбордів та налаштування алертів. Алертинг здійснюється через Alertmanager, що підтримує інтеграцію з Slack, PagerDuty, Email та webhooks для сповіщення команди про проблеми.

Health Check ендпоінти

Health Check — це спеціалізований HTTP-ендпоінт, що повертає інформацію про стан сервісу. У контейнеризованих середовищах (Kubernetes, Docker Swarm) оркестратори використовують health checks для визначення, коли контейнер готовий приймати трафік та чи потрібен рестарт.

Типи Health Checks

1. Liveness Probe (перевірка живості):
Відповідає на питання: "Чи працює застосунок взагалі?" Якщо liveness probe провалюється кілька разів поспіль, оркестратор вбиває контейнер та перезапускає його.

2. Readiness Probe (перевірка готовності):
Відповідає на питання: "Чи готовий застосунок обслуговувати запити?" Застосунок може бути живим, але ще не готовим (наприклад, завантажує дані з БД, прогріває кеш). Readiness probe неуспішна → трафік не надходить до контейнера.

3. Startup Probe (перевірка запуску):
Для застосунків із довгим холодним стартом. Дозволяє відкласти liveness checks до завершення ініціалізації.

Реалізація складного Health Check у Go

package main

import (
    "context"
    "database/sql"
    "encoding/json"
    "net/http"
    "sync"
    "time"

    _ "github.com/lib/pq"
)

// HealthStatus представляє стан окремого компоненту
type HealthStatus struct {
    Status  string                 `json:"status"`  // "healthy", "degraded", "unhealthy"
    Message string                 `json:"message,omitempty"`
    Details map[string]interface{} `json:"details,omitempty"`
}

// HealthResponse агрегує стан всіх компонентів
type HealthResponse struct {
    Status     string                  `json:"status"`
    Version    string                  `json:"version"`
    Uptime     float64                 `json:"uptime_seconds"`
    Timestamp  time.Time               `json:"timestamp"`
    Components map[string]HealthStatus `json:"components"`
}

var (
    startTime = time.Now()
    db        *sql.DB
)

// CheckDatabase перевіряє з'єднання з БД
func CheckDatabase(ctx context.Context) HealthStatus {
    if db == nil {
        return HealthStatus{
            Status:  "unhealthy",
            Message: "Database connection not initialized",
        }
    }

    // Таймаут на перевірку підключення
    // Критично важливо: без таймауту зависла БД заблокує health check назавжди
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    if err := db.PingContext(ctx); err != nil {
        return HealthStatus{
            Status:  "unhealthy",
            Message: "Database ping failed",
            Details: map[string]interface{}{"error": err.Error()},
        }
    }

    // Перевірка кількості активних з'єднань
    stats := db.Stats()
    return HealthStatus{
        Status: "healthy",
        Details: map[string]interface{}{
            "open_connections": stats.OpenConnections,
            "in_use":           stats.InUse,
            "idle":             stats.Idle,
            "max_open":         stats.MaxOpenConnections,
        },
    }
}

### Важливість Context.Context у Health Checks

Використання `context.Context` у функціях перевірки здоров'я є критичним для запобігання каскадним збоям:

**Проблема без таймаутів:**

Уявіть ситуацію: база даних PostgreSQL перестала відповідати через перевантаження або мережеві проблеми. Якщо health check виконує `db.Ping()` без таймауту, виклик заблокується на невизначений час (типово 30–60 секунд або більше). У Kubernetes це призведе до:

1. Readiness probe провалюється після timeout (наприклад, 5 секунд).
2. Под видаляється з балансувальника → трафік перенаправляється на інші поди.
3. **Проте горутина health check все ще блокована!** Вона продовжує споживати ресурси (горутину, з'єднання до БД).
4. При частих запитах health check (кожні 5 секунд) накопичується сотні заблокованих горутин → **витік горутин і з'єднань**.

**Рішення через Context:**

```go
// ❌ Небезпечно: без таймауту
func CheckDatabaseBad() error {
    return db.Ping() // Може блокуватися хвилинами
}

// ✅ Безпечно: з таймаутом через Context
func CheckDatabaseGood(ctx context.Context) error {
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()
    
    return db.PingContext(ctx) // Гарантовано завершиться за 2 секунди
}

Переваги Context у health checks:

  1. Гарантований час виконання: Навіть якщо БД повністю недоступна, перевірка завершиться за заданий час.
  2. Скасування каскадних операцій: Якщо HTTP-запит health check скасовується клієнтом (наприклад, Kubernetes вбив под), Context автоматично скасовує всі дочірні операції (запити до БД, Redis, зовнішніх API).
  3. Запобігання витоку ресурсів: Після спрацювання таймауту драйвер БД закриває з'єднання, звільняючи ресурси пулу.

Приклад складної перевірки з Context:

func CheckExternalAPI(ctx context.Context) HealthStatus {
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()
    
    req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.external.com/health", nil)
    
    client := &http.Client{Timeout: 3 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        // Розрізняємо таймаут vs інші помилки
        if errors.Is(err, context.DeadlineExceeded) {
            return HealthStatus{
                Status:  "unhealthy",
                Message: "External API timeout",
                Details: map[string]interface{}{"timeout": "3s"},
            }
        }
        return HealthStatus{
            Status:  "unhealthy",
            Message: "External API unreachable",
            Details: map[string]interface{}{"error": err.Error()},
        }
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != 200 {
        return HealthStatus{
            Status:  "degraded",
            Message: fmt.Sprintf("External API returned %d", resp.StatusCode),
        }
    }
    
    return HealthStatus{Status: "healthy"}
}
Типова помилка: Створення таймауту, що перевищує timeout самого health check probe. Якщо Kubernetes readiness probe має timeoutSeconds: 5, а ваш health check виконує запит із таймаутом 10 секунд, Kubernetes відміне запит до того, як застосунок поверне відповідь. Завжди встановлюйте внутрішні таймаути менше, ніж probe timeout (наприклад, 2 секунди при 5-секундному probe).

// CheckDiskSpace перевіряє вільне місце на диску (спрощено) func CheckDiskSpace() HealthStatus { // У реальності: syscall.Statfs() на Linux або еквівалент // Для демонстрації повертаємо заглушку freeSpaceGB := 50.0

if freeSpaceGB < 5.0 {
    return HealthStatus{
        Status:  "unhealthy",
        Message: "Critical: Low disk space",
        Details: map[string]interface{}{"free_gb": freeSpaceGB},
    }
} else if freeSpaceGB < 20.0 {
    return HealthStatus{
        Status:  "degraded",
        Message: "Warning: Disk space below threshold",
        Details: map[string]interface{}{"free_gb": freeSpaceGB},
    }
}

return HealthStatus{
    Status:  "healthy",
    Details: map[string]interface{}{"free_gb": freeSpaceGB},
}

}

// HealthCheckHandler обробляє /health запити func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context()

// Паралельна перевірка всіх компонентів
var wg sync.WaitGroup
components := make(map[string]HealthStatus)
mu := sync.Mutex{}

checks := map[string]func() HealthStatus{
    "database": func() HealthStatus { return CheckDatabase(ctx) },
    "disk":     CheckDiskSpace,
}

for name, checkFunc := range checks {
    wg.Add(1)
    go func(name string, check func() HealthStatus) {
        defer wg.Done()
        status := check()
        mu.Lock()
        components[name] = status
        mu.Unlock()
    }(name, checkFunc)
}

wg.Wait()

// Визначення загального статусу
overallStatus := "healthy"
for _, component := range components {
    if component.Status == "unhealthy" {
        overallStatus = "unhealthy"
        break
    } else if component.Status == "degraded" && overallStatus != "unhealthy" {
        overallStatus = "degraded"
    }
}

response := HealthResponse{
    Status:     overallStatus,
    Version:    "v1.2.3",
    Uptime:     time.Since(startTime).Seconds(),
    Timestamp:  time.Now(),
    Components: components,
}

// Статус-код залежить від загального стану
statusCode := http.StatusOK
if overallStatus == "degraded" {
    statusCode = http.StatusOK // 200, але із попередженням
} else if overallStatus == "unhealthy" {
    statusCode = http.StatusServiceUnavailable // 503
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(response)

}

// ReadinessHandler спрощена перевірка готовності func ReadinessHandler(w http.ResponseWriter, r *http.Request) { // Перевірка лише критичних залежностей if db == nil { w.WriteHeader(http.StatusServiceUnavailable) w.Write(byte({"ready": false, "reason": "database not initialized"})) return }

ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
defer cancel()

if err := db.PingContext(ctx); err != nil {
    w.WriteHeader(http.StatusServiceUnavailable)
    w.Write([]byte(`{"ready": false, "reason": "database unavailable"}`))
    return
}

w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ready": true}`))

}

func main() { // Ініціалізація БД (приклад) var err error db, err = sql.Open("postgres", "postgres://user:pass@localhost/dbname?sslmode=disable") if err != nil { panic(err) } defer db.Close()

http.HandleFunc("/health", HealthCheckHandler)
http.HandleFunc("/ready", ReadinessHandler)
http.HandleFunc("/live", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"alive": true}`))
})

http.ListenAndServe(":8080", nil)

}


**Приклад відповіді `/health`:**

```json
{
  "status": "healthy",
  "version": "v1.2.3",
  "uptime_seconds": 3600.5,
  "timestamp": "2026-09-02T14:30:00Z",
  "components": {
    "database": {
      "status": "healthy",
      "details": {
        "open_connections": 5,
        "in_use": 2,
        "idle": 3,
        "max_open": 25
      }
    },
    "disk": {
      "status": "healthy",
      "details": {
        "free_gb": 50
      }
    }
  }
}

Безпека серверних застосунків

Розробка безпечних серверних систем вимагає розуміння типових векторів атак та методів захисту. Проєкт OWASP (Open Web Application Security Project) щорічно публікує список найнебезпечніших вразливостей.

OWASP Top 10 (актуальний список 2021)

  1. Broken Access Control — порушення контролю доступу (доступ до чужих даних).
  2. Cryptographic Failures — слабке шифрування, витік секретів.
  3. Injection — SQL, NoSQL, Command Injection через недовірені дані.
  4. Insecure Design — архітектурні вразливості на етапі проєктування.
  5. Security Misconfiguration — небезпечні налаштування (дефолтні паролі, відкриті порти).
  6. Vulnerable and Outdated Components — використання застарілих бібліотек.
  7. Identification and Authentication Failures — слабка автентифікація.
  8. Software and Data Integrity Failures — відсутність перевірки цілісності.
  9. Security Logging and Monitoring Failures — недостатнє логування атак.
  10. Server-Side Request Forgery (SSRF) — примушення сервера виконувати запити.

Захист паролів через bcrypt

Ніколи не зберігайте паролі у відкритому вигляді або з використанням простих хеш-функцій (MD5, SHA-1). Алгоритм bcrypt забезпечує:

  • Автоматичне солінг: кожен пароль хешується з унікальною випадковою сіллю.
  • Адаптивну складність: параметр cost визначає кількість раундів хешування (зазвичай 10–14). Збільшення на 1 подвоює час обчислення.
  • Захист від rainbow tables та GPU-брутфорсу.
package main

import (
    "fmt"
    "golang.org/x/crypto/bcrypt"
)

// HashPassword генерує bcrypt-хеш пароля
func HashPassword(password string) (string, error) {
    // Cost=12 означає 2^12 = 4096 раундів хешування
    // Кожне збільшення cost на 1 подвоює час обчислення
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
    if err != nil {
        return "", err
    }
    return string(bytes), nil
}

// CheckPassword перевіряє пароль проти хешу
func CheckPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

func main() {
    password := "MySecureP@ssw0rd!"
    
    // Хешування при реєстрації
    hash, _ := HashPassword(password)
    fmt.Println("Хеш:", hash)
    // Виведе: $2a$12$KIXxJ1eV8... (60 символів)
    
    // Перевірка при вході
    isValid := CheckPassword("MySecureP@ssw0rd!", hash)
    fmt.Println("Пароль правильний:", isValid) // true
    
    isValid = CheckPassword("WrongPassword", hash)
    fmt.Println("Неправильний пароль:", isValid) // false
}

Детальний розбір параметра Cost у bcrypt

Параметр cost (або work factor) є ключовою характеристикою bcrypt, що визначає обчислювальну складність хешування. Cost вказує кількість раундів у вигляді степеня двійки: cost=12 означає 2^12 = 4096 ітерацій алгоритму Blowfish.

Таблиця часу обчислення на різних рівнях cost (на сучасному CPU):

CostРаундів (2^cost)Час хешуванняЧас на 1 млн спроб
101,024~65 мс~18 годин
112,048~130 мс~36 годин
124,096~260 мс~3 дні
138,192~520 мс~6 днів
1416,384~1 сек~11.5 днів
1532,768~2 сек~23 дні

Рекомендації щодо вибору cost:

  1. Cost 10–11: Застарілий, небезпечний у 2026 році. Сучасні GPU-ферми можуть перебирати десятки мільйонів хешів за секунду на цьому рівні.
  2. Cost 12 (рекомендований мінімум): Баланс між безпекою та UX. Затримка 200–300 мс при вході/реєстрації є прийнятною для більшості застосунків. Брутфорс 8-символьного пароля займе роки навіть на потужному обладнанні.
  3. Cost 13–14: Для систем із високими вимогами до безпеки (банкінг, медицина, державні установи). Затримка 0.5–1 секунда відчутна, але виправдана для критичних даних.
  4. Cost 15+: Надмірний для більшості випадків. Може призвести до DoS-атак, коли зловмисник масово надсилає запити на хешування, перевантажуючи CPU сервера.

Стратегія міграції cost:

Bcrypt зберігає значення cost у самому хеші (рядок починається з $2a$12$..., де 12 — це cost). При перевірці пароля bcrypt автоматично використовує збережений cost. Це дозволяє поступову міграцію:

func CheckAndRehashIfNeeded(password, oldHash string, db *Database, userID string) (bool, error) {
    // Перевірка пароля з існуючим хешем
    if err := bcrypt.CompareHashAndPassword([]byte(oldHash), []byte(password)); err != nil {
        return false, nil // Неправильний пароль
    }
    
    // Витягання поточного cost із хешу
    cost, _ := bcrypt.Cost([]byte(oldHash))
    
    // Якщо cost застарілий (нижче 12), перехешуємо
    if cost < 12 {
        newHash, err := bcrypt.GenerateFromPassword([]byte(password), 12)
        if err != nil {
            return true, err
        }
        
        // Оновлення хешу в БД
        if err := db.UpdatePasswordHash(userID, string(newHash)); err != nil {
            return true, err
        }
        
        log.Printf("Password rehashed for user %s: cost %d → 12", userID, cost)
    }
    
    return true, nil
}
Динамічний вибір cost на основі потужності сервера: Виконайте бенчмарк при розгортанні застосунку, щоб визначити cost, що забезпечує затримку ~250–500 мс на вашому обладнанні. З часом потужність серверів зростає, і ви можете поступово підвищувати cost для нових паролів.
func BenchmarkBcryptCost() int {
    testPassword := []byte("benchmark_password_test")
    
    for cost := 10; cost <= 16; cost++ {
        start := time.Now()
        bcrypt.GenerateFromPassword(testPassword, cost)
        duration := time.Since(start)
        
        if duration > 300*time.Millisecond {
            return cost - 1 // Повертаємо попередній прийнятний рівень
        }
    }
    
    return 12 // За замовчуванням
}
Ніколи не використовуйте MD5, SHA-1 або SHA-256 для хешування паролів! Ці функції оптимізовані для швидкості, що дозволяє атакувальнику перебирати мільярди варіантів за секунду на сучасному обладнанні. Bcrypt, scrypt та Argon2 спеціально розроблені для повільного обчислення, що робить брутфорс економічно нерентабельним.

Захисні HTTP-заголовки

Додавання спеціалізованих заголовків до HTTP-відповідей блокує цілі класи атак:

// SecurityHeadersMiddleware додає захисні заголовки до відповідей
func SecurityHeadersMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 1. Content-Security-Policy: блокує XSS атаки
        // Дозволяє завантажувати ресурси лише з довірених джерел
        w.Header().Set("Content-Security-Policy", 
            "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:")
        
        // 2. X-Frame-Options: захист від Clickjacking
        // Забороняє вбудовування сторінки у <iframe>
        w.Header().Set("X-Frame-Options", "DENY")
        
        // 3. X-Content-Type-Options: запобігає MIME-sniffing
        // Браузер не намагатиметься "вгадати" тип контенту
        w.Header().Set("X-Content-Type-Options", "nosniff")
        
        // 4. Strict-Transport-Security: примусове HTTPS
        // Браузер автоматично перенаправляє HTTP на HTTPS протягом року
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        
        // 5. X-XSS-Protection: застарілий, але для сумісності
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        
        // 6. Referrer-Policy: контроль передачі Referer
        w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
        
        // 7. Permissions-Policy: обмеження доступу до API браузера
        w.Header().Set("Permissions-Policy", 
            "geolocation=(), microphone=(), camera=()")
        
        next.ServeHTTP(w, r)
    })
}

Пояснення ключових заголовків:

  • Content-Security-Policy (CSP): Дозволяє визначити білий список джерел для скриптів, стилів, зображень тощо. Блокує інлайн-скрипти та eval(), що є основними векторами XSS-атак.
  • X-Frame-Options: Запобігає атаці Clickjacking, коли зловмисник вбудовує ваш сайт у невидимий iframe поверх шахрайської сторінки, примушуючи користувача натискати на прихован елементи.
  • Strict-Transport-Security (HSTS): Після першого HTTPS-з'єднання браузер автоматично використовує лише HTTPS для всіх подальших запитів до домену, навіть якщо користувач введе http://.

Захист від SQL Injection

Завжди використовуйте параметризовані запити замість конкатенації рядків:

// НЕ РОБІТЬ ТАК!
username := r.FormValue("username")
query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s'", username)
rows, _ := db.Query(query)

// Атака: username = "admin' OR '1'='1"
// Результат: SELECT * FROM users WHERE username = 'admin' OR '1'='1'
// Повертає всіх користувачів!

Обмеження частоти запитів (Rate Limiting)

Захист API від DDoS-атак та зловживань через middleware rate limiting:

package main

import (
    "net/http"
    "sync"
    "time"

    "golang.org/x/time/rate"
)

// RateLimiter зберігає лімітери для кожного клієнта (за IP)
type RateLimiter struct {
    limiters map[string]*rate.Limiter
    mu       sync.RWMutex
    r        rate.Limit // Кількість запитів за секунду
    b        int        // Розмір burst bucket
}

func NewRateLimiter(r rate.Limit, b int) *RateLimiter {
    return &RateLimiter{
        limiters: make(map[string]*rate.Limiter),
        r:        r,
        b:        b,
    }
}

// GetLimiter повертає або створює лімітер для IP
func (rl *RateLimiter) GetLimiter(ip string) *rate.Limiter {
    rl.mu.RLock()
    limiter, exists := rl.limiters[ip]
    rl.mu.RUnlock()

    if !exists {
        rl.mu.Lock()
        limiter = rate.NewLimiter(rl.r, rl.b)
        rl.limiters[ip] = limiter
        rl.mu.Unlock()
    }

    return limiter
}

// RateLimitMiddleware обмежує кількість запитів від одного IP
func (rl *RateLimiter) RateLimitMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ip := r.RemoteAddr
        limiter := rl.GetLimiter(ip)

        if !limiter.Allow() {
            http.Error(w, "Rate limit exceeded. Please try again later.", 
                http.StatusTooManyRequests) // 429
            return
        }

        next.ServeHTTP(w, r)
    })
}

func main() {
    // Дозволити 10 запитів/секунду з burst до 20
    rateLimiter := NewRateLimiter(10, 20)

    mux := http.NewServeMux()
    mux.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Data"))
    })

    handler := rateLimiter.RateLimitMiddleware(mux)
    http.ListenAndServe(":8080", handler)
}
Алгоритм Token Bucket (використовується у golang.org/x/time/rate) дозволяє короткочасні сплески трафіку (burst) до заданого ліміту, після чого токени поповнюються з фіксованою швидкістю. Це забезпечує баланс між захистом від зловживань та зручністю для легітимних користувачів.

Контейнеризація Go-застосунків через Docker

Docker революціонізував розгортання застосунків, забезпечуючи ізоляцію, переносимість та консистентність середовищ. Контейнер упаковує застосунок разом із усіма залежностями у самодостатню одиницю, що працює ідентично на машині розробника, тестовому сервері та продакшні.

Переваги контейнеризації Go-застосунків

  1. Статична компіляція: Go-бінарник не вимагає runtime (на відміну від Java/JVM або Python/interpreter), що дозволяє створювати мінімальні образи розміром 5–20 МБ.
  2. Швидкий холодний старт: Відсутність ініціалізації інтерпретатора або JIT-компіляції означає, що контейнер готовий обслуговувати запити за мілісекунди після запуску.
  3. Низьке споживання ресурсів: Go-застосунки ефективно використовують CPU та пам'ять, дозволяючи запускати сотні контейнерів на одному хості.
  4. Безпека через мінімізацію поверхні атаки: Образи без операційної системи (distroless/scratch) не містять оболонки shell, утиліт та бібліотек, що усуває потенційні вразливості.

Наївний Dockerfile: Проблеми та антипатерни

Розглянемо типовий неоптимальний Dockerfile:

# ❌ Антипатерн: використання повного образу Debian
FROM golang:1.22

# Встановлення робочої директорії
WORKDIR /app

# Копіювання коду
COPY . .

# Завантаження залежностей та компіляція
RUN go mod download
RUN go build -o server cmd/server/main.go

# Запуск від імені root (небезпечно!)
CMD ["./server"]

Проблеми цього підходу:

  1. Надмірний розмір образу: Базовий образ golang:1.22 базується на Debian та містить повний Go toolchain, GCC, утиліти (~800 МБ). Фінальний образ може досягати 1 ГБ.
  2. Широка поверхня атаки: Наявність компілятора, оболонки shell та системних утиліт у продакшн-образі створює додаткові вектори експлуатації вразливостей.
  3. Запуск від root: Якщо зловмисник знайде RCE-вразливість, він отримає повний контроль над контейнером.
  4. Неефективне кешування шарів: Docker кешує кожен шар (RUN, COPY). При зміні коду весь шар COPY . . інвалідується, і go mod download виконується заново, навіть якщо залежності не змінилися.

Multi-stage Build: Оптимальний підхід

Багатоетапне складання (Multi-stage Build) розділяє процес на етапи: збірка у повному образі та копіювання лише бінарника у мінімальний продакшн-образ.

# ========================================
# Етап 1: Збірка застосунку (Build Stage)
# ========================================
FROM golang:1.22-alpine AS builder

# Встановлення git (для деяких Go-модулів)
RUN apk add --no-cache git

WORKDIR /build

# Копіювання лише файлів залежностей для кешування
COPY go.mod go.sum ./
RUN go mod download

# Копіювання коду
COPY . .

# Компіляція статичного бінарника
# CGO_ENABLED=0 вимикає C-залежності для повної статичної лінковки
# -ldflags="-w -s" видаляє символьну інформацію та debug-дані (зменшує розмір на ~30%)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
    -ldflags="-w -s" \
    -o server \
    cmd/server/main.go

# ========================================
# Етап 2: Фінальний продакшн-образ
# ========================================
FROM scratch

# Копіювання кореневих сертифікатів для HTTPS-запитів
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Створення non-root користувача (для безпеки)
# У scratch немає /etc/passwd, тому копіюємо з builder
COPY --from=builder /etc/passwd /etc/passwd

# Копіювання скомпільованого бінарника
COPY --from=builder /build/server /app/server

# Створення користувача та групи
USER nobody:nobody

# Експонування порту (документаційна директива)
EXPOSE 8080

# Запуск застосунку
ENTRYPOINT ["/app/server"]

Пояснення ключових оптимізацій:

1. Роздільне копіювання залежностей:

COPY go.mod go.sum ./
RUN go mod download
COPY . .

При зміні коду, але незмінних залежностях Docker використовує кешований шар go mod download, прискорюючи збірку у десятки разів.

2. Прапорці компіляції:

  • CGO_ENABLED=0 — вимикає підтримку C-коду, дозволяючи створити повністю статичний бінарник без зовнішніх бібліотек .so.
  • -ldflags="-w -s" — видаляє налагоджувальну інформацію та таблицю символів, зменшуючи розмір бінарника на 25–40%.

3. Базовий образ scratch: Образ scratch — це абсолютно порожній образ (0 байт). Він не містить жодної ОС, shell, бібліотек — лише ваш статичний бінарник. Розмір фінального образу дорівнює розміру бінарника + SSL-сертифікати (~5–15 МБ).

4. Non-root користувач: Запуск від nobody:nobody (UID 65534) обмежує можливості експлуатації вразливостей. Навіть якщо зловмисник зламає застосунок, він не зможе модифікувати файли або встановлювати програми.

Альтернатива: Distroless образи

Якщо ваш застосунок потребує мінімальних системних залежностей (наприклад, часові зони, локалізація), використовуйте Google Distroless образи:

FROM golang:1.22-alpine AS builder
# ... (етап збірки ідентичний)

FROM gcr.io/distroless/static-debian12:nonroot

COPY --from=builder /build/server /app/server

USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/server"]

Distroless містить мінімальний набір бібліотек (libc, SSL) та метадані часових зон, але не містить shell, package managers, навіть ls чи cat. Розмір: ~2–5 МБ + ваш бінарник.

Для налагодження distroless-образів існує варіант із відладочними утилітами: gcr.io/distroless/static-debian12:debug. Він включає busybox shell для ручної інспекції контейнера. Ніколи не використовуйте debug-варіант у продакшні!

Збірка та запуск Docker-образу

# Збірка образу з тегом
docker build -t my-go-app:v1.0.0 .

# Перевірка розміру образу
docker images my-go-app:v1.0.0
# REPOSITORY    TAG       SIZE
# my-go-app     v1.0.0    12MB  (замість 800MB!)

# Запуск контейнера
docker run -d \
  --name go-server \
  -p 8080:8080 \
  --memory="256m" \
  --cpus="0.5" \
  --restart unless-stopped \
  my-go-app:v1.0.0

# Перегляд логів
docker logs -f go-server

# Зупинка та видалення
docker stop go-server
docker rm go-server

Docker Compose для локальної розробки

Для розробки зручно використовувати Docker Compose, що оркеструє декілька контейнерів (застосунок, БД, Redis):

version: '3.9'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgres://user:password@postgres:5432/mydb?sslmode=disable
      - REDIS_URL=redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped
    networks:
      - app-network

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - app-network

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - app-network

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
    networks:
      - app-network

volumes:
  postgres-data:
  redis-data:

networks:
  app-network:
    driver: bridge

Команди Docker Compose:

# Запуск всіх сервісів у фоновому режимі
docker compose up -d

# Перегляд логів конкретного сервісу
docker compose logs -f app

# Зупинка та видалення контейнерів
docker compose down

# Пересборка після зміни коду
docker compose up -d --build

# Масштабування сервісу (запуск декількох інстансів)
docker compose up -d --scale app=3

Інтеграція з Kubernetes: Deployment та Service

Для продакшн-розгортання Docker-контейнери оркеструються через Kubernetes. Розглянемо базову конфігурацію:

Deployment: Управління репліками застосунку

apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-app-deployment
  labels:
    app: go-app
spec:
  replicas: 3  # Кількість інстансів (для високої доступності)
  selector:
    matchLabels:
      app: go-app
  template:
    metadata:
      labels:
        app: go-app
    spec:
      containers:
      - name: go-server
        image: my-go-app:v1.0.0
        imagePullPolicy: IfNotPresent
        
        ports:
        - containerPort: 8080
          name: http
        
        # Змінні оточення
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: url
        - name: LOG_LEVEL
          value: "info"
        
        # Ліміти ресурсів
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        
        # Health Checks
        livenessProbe:
          httpGet:
            path: /live
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
          timeoutSeconds: 2
          failureThreshold: 3
        
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 3
          periodSeconds: 5
          timeoutSeconds: 2
          successThreshold: 1
          failureThreshold: 2
        
        # Security Context
        securityContext:
          runAsNonRoot: true
          runAsUser: 65534  # nobody
          allowPrivilegeEscalation: false
          capabilities:
            drop:
              - ALL
          readOnlyRootFilesystem: true

Пояснення ключових параметрів:

Ліміти ресурсів (resources):

  • requests — мінімальні ресурси, гарантовані контейнеру. Kubernetes не запустить под на вузлі без достатніх ресурсів.
  • limits — максимальні ресурси. При перевищенні CPU контейнер throttle-иться, при перевищенні пам'яті — вбивається (OOMKilled).

Liveness Probe: Kubernetes періодично перевіряє /live. Після 3 невдалих спроб (failureThreshold) под перезапускається. Це захищає від deadlock та зависань застосунку.

Readiness Probe: Під вважається готовим приймати трафік лише після успішної перевірки /ready. Якщо probe провалюється, под видаляється з балансувальника навантаження, але не перезапускається (на відміну від liveness).

Security Context:

  • runAsNonRoot: true — Kubernetes відхилить запуск контейнера від root.
  • readOnlyRootFilesystem: true — файлова система монтується лише для читання, запобігаючи модифікації бінарника зловмисником.
  • capabilities.drop: ALL — видалення всіх Linux capabilities (наприклад, CAP_NET_BIND_SERVICE), обмежуючи можливості контейнера.

Service: Експонування застосунку

apiVersion: v1
kind: Service
metadata:
  name: go-app-service
spec:
  type: ClusterIP  # Внутрішній IP у кластері (для зовнішнього доступу: LoadBalancer)
  selector:
    app: go-app
  ports:
  - port: 80        # Порт, що слухає Service
    targetPort: 8080 # Порт контейнера
    protocol: TCP
    name: http

Service виконує роль внутрішнього балансувальника навантаження: розподіляє запити між усіма здоровими подами з міткою app: go-app.

Ingress: Маршрутизація зовнішнього трафіку

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: go-app-ingress
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - api.example.com
    secretName: go-app-tls
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: go-app-service
            port:
              number: 80

Ingress маршрутизує HTTP/HTTPS трафік із зовнішнього світу до внутрішніх сервісів на основі доменних імен та шляхів. Інтеграція з cert-manager автоматизує отримання та оновлення Let's Encrypt SSL-сертифікатів.


Практичні рекомендації та найкращі практики

1. .dockerignore для прискорення збірки

Файл .dockerignore виключає непотрібні файли з контексту збірки:

# Виключити Git-репозиторій
.git
.gitignore

# Виключити локальні налаштування
.env
*.local

# Виключити тести та документацію
*_test.go
docs/
README.md

# Виключити директорії збірки
bin/
dist/
vendor/

# Виключити Docker-файли
Dockerfile*
docker-compose.yml

2. Версіонування образів

Використовуйте семантичне версіонування та додавайте git commit SHA:

VERSION=$(git describe --tags --always)
docker build -t my-go-app:${VERSION} -t my-go-app:latest .

3. Сканування вразливостей

Регулярно скануйте образи на вразливості:

# Trivy — популярний сканер вразливостей
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy:latest image my-go-app:v1.0.0

# Snyk
snyk container test my-go-app:v1.0.0

Інтерпретація результатів сканування Trivy

Trivy класифікує вразливості за рівнями серйозності (CVSS Score):

Приклад виходу Trivy:

my-go-app:v1.0.0 (alpine 3.18.4)

Total: 5 (UNKNOWN: 0, LOW: 2, MEDIUM: 2, HIGH: 1, CRITICAL: 0)

┌─────────────┬──────────────┬──────────┬───────────────────┬──────────────┬────────────────────────┐
│   Library   │ Vulnerability│ Severity │ Installed Version │ Fixed Version│        Title           │
├─────────────┼──────────────┼──────────┼───────────────────┼──────────────┼────────────────────────┤
│ libcrypto3  │ CVE-2023-5678│ HIGH     │ 3.1.3-r0          │ 3.1.4-r0     │ OpenSSL: Buffer Overflow│
│ libssl3     │ CVE-2023-5678│ HIGH     │ 3.1.3-r0          │ 3.1.4-r0     │ OpenSSL: Buffer Overflow│
│ busybox     │ CVE-2023-1234│ MEDIUM   │ 1.36.1-r2         │ 1.36.1-r5    │ BusyBox: Path Traversal │
└─────────────┴──────────────┴──────────┴───────────────────┴──────────────┴────────────────────────┘

Рівні серйозності та дії:

SeverityCVSS ScoreОписДія
CRITICAL9.0–10.0RCE, privilege escalation, повний компроміс системиБлокувати деплой! Терміново оновити залежності.
HIGH7.0–8.9Витік чутливих даних, DoS, обхід автентифікаціїОновити протягом 24–48 годин.
MEDIUM4.0–6.9Витік інформації, обмежений DoSОновити у наступному релізі (1–2 тижні).
LOW0.1–3.9Мінімальний вплив, складна експлуатаціяОновити при можливості, не критично.

Стратегія виправлення:

  1. Оновлення базового образу:
# Замість фіксованої версії
FROM alpine:3.18.4

# Використовуйте останню патч-версію
FROM alpine:3.18

# Або останню мінорну (для non-breaking changes)
FROM alpine:3
  1. Виключення non-applicable вразливостей: Якщо вразливість присутня у бібліотеці, яку ваш застосунок не використовує (наприклад, Apache у образі, але ви запускаєте лише Go-бінарник), створіть .trivyignore:
# .trivyignore
CVE-2023-1234  # BusyBox не використовується у scratch-образі
  1. Автоматизація сканування у CI/CD:
# GitLab CI
security_scan:
  image: aquasec/trivy:latest
  script:
    - trivy image --exit-code 1 --severity CRITICAL,HIGH $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  only:
    - merge_requests
    - main

Параметр --exit-code 1 змушує pipeline провалитися при виявленні CRITICAL/HIGH вразливостей, блокуючи деплой небезпечного коду у продакшн.

False Positives: Сканери іноді помилково детектують вразливості у статично зібраних бінарниках. Приклад: Trivy може виявити CVE у libc, хоча ваш Go-бінарник з CGO_ENABLED=0 не містить жодних C-залежностей. Завжди перевіряйте, чи дійсно вразливість застосовна до вашого контексту.

4. Використання BuildKit для прискорення

Активуйте експериментальний BuildKit для паралельної збірки та кешування:

export DOCKER_BUILDKIT=1
docker build --build-arg GO_VERSION=1.22 -t my-go-app:v1.0.0 .

5. Оптимізація для CI/CD

У CI-pipeline кешуйте шари між зборками:

# GitLab CI приклад
build:
  image: docker:latest
  services:
    - docker:dind
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build --cache-from $CI_REGISTRY_IMAGE:latest -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

Висновки та майбутні напрямки

📊 Спостережуваність

Впровадження структурованого логування через log/slog, збір метрик у Prometheus та health checks забезпечують повну видимість стану системи. Інтеграція з Grafana дозволяє створювати дашборди для виявлення аномалій та оптимізації продуктивності.

🔒 Безпека

Захист серверних застосунків вимагає комплексного підходу: криптографічне хешування паролів через bcrypt, захисні HTTP-заголовки, параметризовані SQL-запити та rate limiting. Регулярний аудит залежностей та сканування вразливостей є обов'язковими практиками.

🐳 Контейнеризація

Multi-stage Docker збірки дозволяють створювати мінімальні безпечні образи (<15 МБ) на базі scratch або distroless. Запуск від non-root користувача та readonly файлова система радикально зменшують поверхню атаки. Інтеграція з Kubernetes забезпечує автоматичне масштабування та самовідновлення.

Еволюція технологій

Індустрія розвивається у напрямку eBPF-based observability (Cilium, Pixie), що дозволяє збирати метрики на рівні ядра Linux без модифікації коду застосунків. Service Mesh (Istio, Linkerd) автоматизують mTLS, трасування та ретраї між мікросервісами. Serverless Containers (AWS Fargate, Google Cloud Run) усувають необхідність управління інфраструктурою, запускаючи контейнери on-demand із автоматичним масштабуванням до нуля.


Контрольні питання для самоперевірки


Ресурси для подальшого вивчення

Офіційна документація

Книги та посібники

  • "Observability Engineering" by Charity Majors, Liz Fong-Jones, George Miranda — фундаментальна праця про спостережуваність розподілених систем.
  • "Security Engineering" by Ross Anderson — комплексний огляд інженерії безпеки.
  • "Kubernetes Patterns" by Bilgin Ibryam, Roland Huß — патерни проєктування для контейнеризованих застосунків.

Інструменти та бібліотеки

Онлайн-ресурси


Завершення курсу: Від теорії до практики

Вітаємо з завершенням курсу "Розробка застосувань клієнт-серверної архітектури"! Протягом 18 лекцій ми здійснили всебічну подорож від фундаментальних основ мови Go та низькорівневого сокетного програмування до створення промислових систем моніторингу, безпеки та контейнеризації.

Ключові досягнення курсу

Етап 1. Фундаментальні знання (Лекції 1–3)

Опановано систему типів Go, модель пам'яті (стек, купа, Escape Analysis), парадигму конкурентності CSP (горутини, канали, примітиви sync) та принципи качиної типізації через інтерфейси.

Етап 2. Мережеві протоколи (Лекції 4–7)

Реалізовано багатопотокові TCP/UDP сервери, налаштовано криптографічний захист через TLS/mTLS, опрацьовано прикладні протоколи DNS та SMTP з розумінням RFC-специфікацій.

Етап 3. Вебсервіси та REST API (Лекції 8–12)

Створено стандартизовані RESTful API на базі net/http, Chi та Gin із автоматичною валідацією DTO, JWT-автентифікацією та автогенерацією Swagger-документації.

Етап 4. Бази даних та розподілені системи (Лекції 13–16)

Інтегровано PostgreSQL через pgx/GORM, реалізовано комунікацію у реальному часі через WebSockets, міжсервісну взаємодію через gRPC та асинхронний обмін повідомленнями через RabbitMQ/MQTT.

Етап 5. Потокові технології (Лекція 17)

Опановано протоколи HLS/DASH для адаптивного стрімінгу та технологію WebRTC для прямого P2P зв'язку із розумінням ICE, STUN/TURN та реалізацією на базі Pion.

Етап 6. Продакшн-готовність (Лекція 18)

Впровадили три стовпи спостережуваності (логування, метрики, трасування), захистили застосунки від OWASP Top 10 вразливостей та контейнеризували через Docker із мінімальними безпечними образами.

Напрямки подальшого розвитку

Після завершення курсу рекомендуємо:

  1. Створити власний pet-project: Повноцінна система (наприклад, платформа відеоконференцій, API для IoT-пристроїв, мікросервісна архітектура e-commerce) із застосуванням усіх вивчених технологій.
  2. Вивчити Service Mesh: Istio або Linkerd для автоматизації mTLS, ретраїв, circuit breaking та трасування між мікросервісами.
  3. Опанувати GitOps: ArgoCD або Flux для декларативного управління Kubernetes через Git.
  4. Поглибити знання безпеки: Сертифікація CKS (Certified Kubernetes Security Specialist) або вивчення атак на рівні контейнерів.
  5. Дослідити нові парадигми: Serverless (AWS Lambda, Google Cloud Functions), Edge Computing, WASM для серверного виконання.
Практика є ключем до майстерності. Теоретичні знання закріплюються лише через реальні проєкти, участь у open-source спільнотах та вирішення реальних виробничих проблем. Створюйте, експериментуйте та ніколи не припиняйте вчитися!

🎓 Досягнуто рівень

Junior → Middle Go Backend Engineer

Ви володієте фундаментальними знаннями та практичними навичками для розробки, розгортання та підтримки промислових Go-застосунків. Готові до співбесід на позиції backend-розробника та участі у реальних командних проєктах.

🚀 Наступний крок

Від мікросервісів до розподілених систем

Вивчайте масштабування горизонтальне (sharding БД, event sourcing, CQRS), розподілену координацію (etcd, Consul), обробку великих даних (Apache Kafka, event streaming) та побудову fault-tolerant архітектур.


Успіхів у розробці надійних, безпечних та масштабованих систем! 🎉

Copyright © 2026