Tarkibga o'tish

Config va .env bilan ishlash

Oldingi darslarda baza manzili os.Getenv("DATABASE_URL") orqali olindi. Lekin API’da sozlamalar bittadan ancha ko‘p bo‘ladi: port, baza manzili, JWT maxfiy kaliti, timeoutlar, log darajasi, CORS uchun ruxsat etilgan domenlar.

Bu qiymatlar muhitga qarab farq qiladi. Kompyuteringizda baza localhostda, test serverida boshqa manzilda, production’da esa butunlay boshqa joyda turadi. Kod esa hamma joyda bir xil bo‘lishi kerak.

Bu darsda sozlamalarni koddan ajratish, ularni bitta joyda o‘qish, tekshirish va xavfsiz saqlashni o‘rganamiz. Muhit o‘zgaruvchilari bilan ishlash asoslari Environment o‘zgaruvchilari darsida berilgan. Bu yerda ularni API loyihasida qo‘llaymiz.

Nima uchun kodga yozib bo‘lmaydi?

// ❌ Hech qachon bunday qilmang
db, _ := sql.Open("pgx", "postgres://app:[email protected]:5432/books")

Bu kodda uchta muammo bor:

  1. Parol Git tarixida qoladi. Keyinroq o‘chirilsa ham, u eski commit’larda saqlanib qoladi. Repozitoriyga kirish huquqi bor har bir kishi, jumladan sobiq xodimlar ham, uni ko‘ra oladi.
  2. Muhitni almashtirish uchun kodni o‘zgartirish kerak. Test serveriga chiqarish uchun qaytadan kompilyatsiya qilishga to‘g‘ri keladi.
  3. Parolni almashtirish qiyinlashadi. Parol sizib chiqsa, uni yangilash uchun kodni o‘zgartirib, qaytadan deploy qilish kerak.

Keng tarqalgan Twelve-Factor App metodologiyasida bu qoida shunday ifodalangan: konfiguratsiya muhit o‘zgaruvchilarida saqlanadi. Bitta build qilingan dastur istalgan muhitda ishlaydi, faqat unga berilgan muhit o‘zgaruvchilari farq qiladi.

Config struct

Birinchi qadam — barcha sozlamalarni bitta struct’ga yig‘ish:

type Config struct {
    Env          string        // development, staging, production
    HTTPAddr     string        // ":8080"
    DatabaseURL  string        // postgres://...
    JWTSecret    string        // token imzolash kaliti
    ReadTimeout  time.Duration // HTTP server timeouti
    DBMaxConns   int32         // ulanishlar puli hajmi
    CORSOrigins  []string      // ruxsat etilgan domenlar
    LogLevel     slog.Level    // debug, info, warn, error
}

Afzalliklari:

  • dastur qanday sozlamalarga muhtojligi bitta joyda ko‘rinib turadi;
  • har bir qiymat kerakli turda saqlanadi: time.Duration, int32, []string;
  • os.Getenv kod bo‘ylab tarqalib ketmaydi.

O‘qish va tekshirish

Sozlamalar dastur ishga tushganda bir marta o‘qiladi va darhol tekshiriladi. Biror majburiy qiymat yo‘q bo‘lsa, dastur ishga tushmasligi kerak.

Nima uchun darhol? JWT kaliti berilmagan server ishga tushib, faqat birinchi login so‘rovida xato bersa, muammo kechikib aniqlanadi. Ishga tushish paytidagi aniq xato esa deploy bosqichidayoq ko‘rinadi.

Barcha xatolarni birma-bir emas, bir vaqtda ko‘rsatish ham muhim. Aks holda bitta xatoni tuzatib qayta ishga tushirasiz, keyingisi chiqadi va bu jarayon takrorlanaveradi.

main.go
package main

import (
    "errors"
    "fmt"
    "log/slog"
    "os"
    "strconv"
    "strings"
    "time"
)

type Config struct {
    Env         string
    HTTPAddr    string
    DatabaseURL string
    JWTSecret   string
    ReadTimeout time.Duration
    DBMaxConns  int32
    CORSOrigins []string
    LogLevel    slog.Level
}

// loader xatolarni yig‘ib boradi, birinchi xatoda to‘xtamaydi.
type loader struct {
    errs []error
}

func (l *loader) str(key, def string) string {
    if v, ok := os.LookupEnv(key); ok && v != "" {
        return v
    }
    return def
}

func (l *loader) required(key string) string {
    v := os.Getenv(key)
    if v == "" {
        l.errs = append(l.errs, fmt.Errorf("%s majburiy", key))
    }
    return v
}

func (l *loader) duration(key string, def time.Duration) time.Duration {
    v := os.Getenv(key)
    if v == "" {
        return def
    }
    d, err := time.ParseDuration(v)
    if err != nil {
        l.errs = append(l.errs, fmt.Errorf("%s davomiylik bo‘lishi kerak (masalan 5s, 1m)", key))
    }
    return d
}

func (l *loader) int32(key string, def int32) int32 {
    v := os.Getenv(key)
    if v == "" {
        return def
    }
    n, err := strconv.ParseInt(v, 10, 32)
    if err != nil || n <= 0 {
        l.errs = append(l.errs, fmt.Errorf("%s musbat butun son bo‘lishi kerak", key))
    }
    return int32(n)
}

func (l *loader) list(key string) []string {
    var out []string
    for _, s := range strings.Split(os.Getenv(key), ",") {
        if s = strings.TrimSpace(s); s != "" {
            out = append(out, s)
        }
    }
    return out
}

func Load() (Config, error) {
    var l loader

    cfg := Config{
        Env:         l.str("APP_ENV", "development"),
        HTTPAddr:    l.str("HTTP_ADDR", ":8080"),
        DatabaseURL: l.required("DATABASE_URL"),
        JWTSecret:   l.required("JWT_SECRET"),
        ReadTimeout: l.duration("HTTP_READ_TIMEOUT", 10*time.Second),
        DBMaxConns:  l.int32("DB_MAX_CONNS", 25),
        CORSOrigins: l.list("CORS_ORIGINS"),
    }

    if err := cfg.LogLevel.UnmarshalText([]byte(l.str("LOG_LEVEL", "info"))); err != nil {
        l.errs = append(l.errs, errors.New("LOG_LEVEL debug, info, warn yoki error bo‘lishi kerak"))
    }

    switch cfg.Env {
    case "development", "staging", "production":
    default:
        l.errs = append(l.errs, fmt.Errorf("APP_ENV noto‘g‘ri: %q", cfg.Env))
    }

    if cfg.JWTSecret != "" && len(cfg.JWTSecret) < 32 {
        l.errs = append(l.errs, errors.New("JWT_SECRET kamida 32 belgi bo‘lishi kerak"))
    }

    return cfg, errors.Join(l.errs...)
}

func main() {
    cfg, err := Load()
    if err != nil {
        fmt.Fprintf(os.Stderr, "konfiguratsiya xatosi:\n%v\n", err)
        os.Exit(1)
    }

    fmt.Printf("Muhit: %s, manzil: %s, pul: %d, timeout: %v, CORS: %v\n",
        cfg.Env, cfg.HTTPAddr, cfg.DBMaxConns, cfg.ReadTimeout, cfg.CORSOrigins)
}

Hech narsa bermasdan ishga tushiramiz:

go run main.go
konfiguratsiya xatosi:
DATABASE_URL majburiy
JWT_SECRET majburiy

Noto‘g‘ri qiymatlar bilan:

DATABASE_URL=postgres://localhost/books JWT_SECRET=qisqa \
DB_MAX_CONNS=ko‘p HTTP_READ_TIMEOUT=10 go run main.go
konfiguratsiya xatosi:
HTTP_READ_TIMEOUT davomiylik bo‘lishi kerak (masalan 5s, 1m)
DB_MAX_CONNS musbat butun son bo‘lishi kerak
JWT_SECRET kamida 32 belgi bo‘lishi kerak

To‘g‘ri qiymatlar bilan:

DATABASE_URL=postgres://localhost/books \
JWT_SECRET=$(openssl rand -hex 32) \
CORS_ORIGINS="https://go-lang.uz, https://admin.go-lang.uz" \
go run main.go
Muhit: development, manzil: :8080, pul: 25, timeout: 10s, CORS: [https://go-lang.uz https://admin.go-lang.uz]

Kod qanday ishladi:

  • loader har bir xatoni yig‘ib boradi va birinchi xatoda to‘xtamaydi.
  • errors.Join barcha xatolarni bittaga birlashtiradi. Har bir xato alohida qatorda chiqadi.
  • Ixtiyoriy qiymatlar uchun standart qiymat berildi. Majburiy qiymatlar uchun esa standart qiymat yo‘q: DATABASE_URL uchun "taxminiy" qiymat bo‘lmaydi.
  • openssl rand -hex 32 32 baytlik tasodifiy kalit yaratadi. JWT kalitini aynan shunday yaratish kerak, uni o‘ylab topish emas.

.env fayli

Ishlab chiqish paytida har safar o‘nlab o‘zgaruvchini terminalga yozish noqulay. Shuning uchun ular loyiha ildizidagi .env fayliga yoziladi:

.env
APP_ENV=development
HTTP_ADDR=:8080
DATABASE_URL=postgres://app:secret@localhost:5432/books?sslmode=disable
JWT_SECRET=8f3c1a9e7b2d4f6a0c5e9b1d3f7a2c4e6b8d0f1a3c5e7b9d2f4a6c8e0b1d3f5a
LOG_LEVEL=debug
CORS_ORIGINS=http://localhost:3000

Go .env faylini o‘zi o‘qimaydi. Buning uchun github.com/joho/godotenv paketi keng qo‘llanadi:

go get github.com/joho/godotenv
import "github.com/joho/godotenv"

func main() {
    // .env bo‘lsa yuklanadi, bo‘lmasa jimgina o‘tib ketiladi
    if err := godotenv.Load(); err != nil && !errors.Is(err, fs.ErrNotExist) {
        log.Fatalf(".env faylini o‘qib bo‘lmadi: %v", err)
    }

    cfg, err := Load()
    // ...
}

godotenv.Load() .env faylidagi qiymatlarni jarayon muhitiga yozadi. Shundan keyin os.Getenv ularni ko‘radi.

Muhim jihatlar:

  • Mavjud o‘zgaruvchilar ustidan yozilmaydi. Muhitda DATABASE_URL allaqachon bo‘lsa, .envdagi qiymat e’tiborsiz qoladi. Shu sababli production’da serverning haqiqiy muhit o‘zgaruvchilari doim ustun turadi.
  • Fayl bo‘lmasligi xato emas. Production’da .env fayli bo‘lmaydi, qiymatlar to‘g‘ridan-to‘g‘ri muhitdan keladi. Shuning uchun fs.ErrNotExist alohida tekshiriladi.
  • godotenv faqat main paketida chaqiriladi. Kutubxona va boshqa paketlar muhitni o‘zgartirmasligi kerak.

.envni Git’dan himoyalash

.env faylida parollar bor. U hech qachon repozitoriyga tushmasligi kerak:

.gitignore
.env
.env.*
!.env.example

Oxirgi qator .env.example faylini istisno qiladi. Bu fayl commit qilinadi va loyihaga qanday o‘zgaruvchilar kerakligini ko‘rsatadi. Unda maxfiy qiymatlar bo‘lmaydi:

.env.example
APP_ENV=development
HTTP_ADDR=:8080
# postgres://USER:PASSWORD@HOST:PORT/DB?sslmode=disable
DATABASE_URL=
# openssl rand -hex 32 buyrug‘i bilan yarating
JWT_SECRET=
LOG_LEVEL=info
CORS_ORIGINS=http://localhost:3000

Yangi dasturchi loyihani yuklab olgach, cp .env.example .env bajaradi va qiymatlarni to‘ldiradi.

Danger

Maxfiy qiymat bir marta commit qilinib, GitHub’ga yuklangan bo‘lsa, faylni o‘chirish yetarli emas. Uni zudlik bilan almashtiring: yangi parol, yangi JWT kaliti. Ochiq repozitoriylarni avtomatik skanerlaydigan botlar sizib chiqqan kalitlarni bir necha daqiqada topadi.

Maxfiy qiymatlarni logdan yashirish

Sozlamalarni debug uchun logga chiqarish qulay. Lekin slog.Info("config", "cfg", cfg) qatori JWT kalitini ham logga yozib yuboradi.

Himoya usullaridan biri — maxfiy qiymatlar uchun maxsus tur yaratish:

main.go
package main

import (
    "fmt"
    "log/slog"
    "os"
)

// Secret — logda va fmt chiqishida yashiriladigan satr.
type Secret string

func (s Secret) String() string {
    if s == "" {
        return ""
    }
    return "[YASHIRILGAN]"
}

func (s Secret) LogValue() slog.Value {
    return slog.StringValue(s.String())
}

// Reveal haqiqiy qiymatni faqat ongli ravishda olish uchun.
func (s Secret) Reveal() string { return string(s) }

type Config struct {
    HTTPAddr    string
    DatabaseURL Secret
    JWTSecret   Secret
}

func main() {
    cfg := Config{
        HTTPAddr:    ":8080",
        DatabaseURL: "postgres://app:secret@localhost/books",
        JWTSecret:   "8f3c1a9e7b2d4f6a",
    }

    fmt.Printf("%+v\n", cfg)

    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    logger.Info("sozlamalar", "addr", cfg.HTTPAddr, "db", cfg.DatabaseURL, "jwt", cfg.JWTSecret)

    // Haqiqiy qiymat kerak bo‘lgan joyda:
    _ = cfg.JWTSecret.Reveal()
}
{HTTPAddr::8080 DatabaseURL:[YASHIRILGAN] JWTSecret:[YASHIRILGAN]}
{"time":"...","level":"INFO","msg":"sozlamalar","addr":":8080","db":"[YASHIRILGAN]","jwt":"[YASHIRILGAN]"}
  • String() metodi fmt paketi uchun ishlaydi.
  • LogValue() metodi slog.LogValuer interfeysini amalga oshiradi va slog uchun ishlaydi.
  • Haqiqiy qiymat faqat Reveal() orqali olinadi. Bu nom kodni o‘qiyotgan odamga maxfiy qiymat ishlatilayotganini aniq ko‘rsatadi.

Config’ni qanday uzatish kerak?

Configni global o‘zgaruvchiga saqlab, istalgan joydan o‘qish vasvasasi bor. Lekin clean architecture darsidagi kabi, bog‘liqliklar ochiq uzatilgani yaxshiroq:

func main() {
    cfg, err := config.Load()
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    pool, err := newPool(ctx, cfg.DatabaseURL.Reveal(), cfg.DBMaxConns)
    // ...
    tokens := auth.NewTokenService(cfg.JWTSecret.Reveal(), 15*time.Minute)
    // ...
    srv := &http.Server{
        Addr:        cfg.HTTPAddr,
        ReadTimeout: cfg.ReadTimeout,
        // ...
    }
}

Har bir komponent faqat o‘ziga kerakli qiymatlarni oladi. Token servisi baza manzilini bilmaydi, repository esa JWT kalitini bilmaydi. Testlarda ham kerakli qiymatni to‘g‘ridan-to‘g‘ri berish mumkin, muhit o‘zgaruvchilarini sozlash shart emas.

Production’da maxfiy qiymatlar

Serverda qiymatlar turli yo‘llar bilan beriladi:

Muhit Qanday beriladi
systemd servis EnvironmentFile=/etc/books-api/env (fayl huquqlari 600)
Docker docker run --env-file yoki -e
Docker Compose environment: yoki env_file: bo‘limi
Kubernetes Secret obyekti orqali
Bulut AWS Secrets Manager, Google Secret Manager, HashiCorp Vault

Maxfiy qiymatlarni markazlashgan boshqaruvchida (secret manager) saqlash eng ishonchli usul. U kim qachon qaysi kalitni o‘qiganini qayd etadi va kalitlarni avtomatik almashtirish imkonini beradi.

Xulosa

  • Sozlamalar koddan ajratiladi va muhit o‘zgaruvchilari orqali beriladi.
  • Barcha sozlamalar bitta Config struct’ida, kerakli turlarda saqlanadi.
  • Config dastur boshida bir marta o‘qiladi, tekshiriladi va barcha xatolar birdaniga ko‘rsatiladi.
  • Ishlab chiqishda .env va godotenv qulay. .env Git’ga tushmaydi, uning o‘rniga .env.example commit qilinadi.
  • Maxfiy qiymatlar uchun alohida tur logga va fmt chiqishiga tushishining oldini oladi.
  • Config global o‘zgaruvchi orqali emas, komponentlarga parametr sifatida uzatiladi.

Shu bilan ma’lumotlar bazasi bo‘limi tugadi. Keyingi bo‘limda foydalanuvchilarni tanish — autentifikatsiya mavzusini boshlaymiz.