Node.js ile Environment Variable Yönetimi

Production ortamına deploy ettiğin uygulamanın database şifresi ya da API key’i kaynak koduna gömülü halde GitHub’a gitmiş mi? O an hissettirdiği panik duygusunu yaşamamak için environment variable yönetimini doğru öğrenmek şart. Node.js ekosisteminde bu konuyu düzgün halletmek hem güvenlik hem de farklı ortamlar arasında sorunsuz geçiş yapabilmek açısından kritik.

Environment Variable Nedir ve Neden Önemli?

Environment variable’lar, işletim sistemi seviyesinde tutulan anahtar-değer çiftleridir. Uygulaman çalışırken bu değerlere process.env nesnesi üzerinden erişirsin. Temel fikir şu: ortama özgü konfigürasyon bilgilerini (veritabanı bağlantı adresi, port numarası, API anahtarları, gizli şifreler) kaynak kodundan ayırmak.

Bu ayrımın sağladığı faydalar:

  • Güvenlik: Hassas bilgilerin git repository’sine gitmesini önlersin
  • Esneklik: Aynı kod tabanı development, staging ve production ortamlarında farklı konfigürasyonlarla çalışır
  • 12-Factor App prensibi: Endüstri standardı uygulama geliştirme metodolojisine uyarsın
  • Operasyonel kolaylık: Kodu yeniden deploy etmeden konfigürasyonu değiştirebilirsin

process.env ile Temel Kullanım

Node.js’te herhangi bir environment variable’a erişmek için process.env kullanırsın. Basit bir örnek:

# Terminal üzerinden geçici olarak set etmek
export DB_HOST=localhost
export DB_PORT=5432
export DB_NAME=myapp
node app.js
# Tek satırda uygulama başlatırken set etmek
DB_HOST=localhost DB_PORT=5432 node app.js

JavaScript tarafında bunu şöyle okursun:

# app.js içeriği (node ile çalıştırılacak)
cat > app.js << 'EOF'
const dbHost = process.env.DB_HOST;
const dbPort = process.env.DB_PORT || 5432;
const dbName = process.env.DB_NAME || 'development_db';
const nodeEnv = process.env.NODE_ENV || 'development';

console.log(`Ortam: ${nodeEnv}`);
console.log(`Veritabanı: ${dbHost}:${dbPort}/${dbName}`);

if (!dbHost) {
  console.error('HATA: DB_HOST tanımlanmamış!');
  process.exit(1);
}
EOF
node app.js

Burada dikkat etmen gereken bir şey var: process.env üzerinden okunan her değer string tipinde gelir. Port numarası veya benzeri sayısal değerleri kullanmadan önce dönüştürmen gerekir.

dotenv Paketi ile Çalışmak

Gerçek dünyada her ortam değişkenini terminal üzerinden export etmek pratik değil. Bu sorunu çözmek için .env dosyası ve dotenv paketi kullanılır.

# dotenv paketini kur
npm install dotenv

# Geliştirme bağımlılığı olarak kurabilirsin de
npm install --save-dev dotenv

Proje kök dizininde .env dosyası oluşturursun:

cat > .env << 'EOF'
NODE_ENV=development
PORT=3000
DB_HOST=localhost
DB_PORT=5432
DB_USER=appuser
DB_PASSWORD=supersecretpassword
DB_NAME=myapp_dev
JWT_SECRET=my-very-long-and-random-jwt-secret-key
REDIS_URL=redis://localhost:6379
SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxx
EOF

Uygulamanın entry point dosyasında, başka hiçbir şeyden önce dotenv’i yüklersin:

cat > server.js << 'EOF'
// Bu satır EN ÜSTTE olmalı
require('dotenv').config();

const express = require('express');
const app = express();

const port = parseInt(process.env.PORT, 10) || 3000;
const jwtSecret = process.env.JWT_SECRET;

if (!jwtSecret) {
  throw new Error('JWT_SECRET environment variable tanımlanmamış!');
}

app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    environment: process.env.NODE_ENV,
    timestamp: new Date().toISOString()
  });
});

app.listen(port, () => {
  console.log(`Sunucu ${process.env.NODE_ENV} modunda port ${port} üzerinde çalışıyor`);
});
EOF

.gitignore ile Güvenlik

.env dosyasını oluşturduktan sonra yapman gereken ilk ve en kritik adım onu git takibinden çıkarmak:

# .gitignore dosyasına ekle
cat >> .gitignore << 'EOF'

# Environment variables
.env
.env.local
.env.*.local
.env.production
.env.staging
EOF

# Eğer .env zaten commit'lendiyse git cache'den temizle
git rm --cached .env
git commit -m "Remove .env from tracking"

Bunun yerine .env.example veya .env.template adında, gerçek değerler içermeyen bir şablon dosyası oluştururun ve bunu git’e eklersin:

cat > .env.example << 'EOF'
NODE_ENV=development
PORT=3000
DB_HOST=
DB_PORT=5432
DB_USER=
DB_PASSWORD=
DB_NAME=
JWT_SECRET=
REDIS_URL=
SENDGRID_API_KEY=
EOF

git add .env.example
git commit -m "Add .env.example template"

Ekibe yeni katılan birisi projeyi clone’ladığında cp .env.example .env yapıp kendi değerlerini doldurur. Bu yaklaşım hem güvenli hem de onboarding sürecini kolaylaştırır.

Ortama Göre Farklı Konfigürasyon Dosyaları

Büyük projelerde development, test, staging ve production için ayrı konfigürasyon dosyaları tutmak gerekir. dotenv bunu destekler:

# Ortam bazlı .env dosyaları
touch .env.development
touch .env.test
touch .env.staging
# .env.production GIT'E GİTMEZ!

# .env.development içeriği
cat > .env.development << 'EOF'
DB_HOST=localhost
DB_PORT=5432
LOG_LEVEL=debug
CACHE_TTL=60
EOF

# .env.test içeriği
cat > .env.test << 'EOF'
DB_HOST=localhost
DB_PORT=5433
DB_NAME=myapp_test
LOG_LEVEL=error
CACHE_TTL=0
EOF

Node.js tarafında hangi dosyanın yükleneceğini NODE_ENV‘e göre belirlersin:

cat > config/env.js << 'EOF'
const path = require('path');
const dotenv = require('dotenv');

const nodeEnv = process.env.NODE_ENV || 'development';

// Önce ortama özgü dosyayı yükle, sonra genel .env'i
const envFile = path.resolve(process.cwd(), `.env.${nodeEnv}`);
const defaultEnvFile = path.resolve(process.cwd(), '.env');

dotenv.config({ path: envFile });
dotenv.config({ path: defaultEnvFile });

console.log(`Konfigürasyon yüklendi: ${nodeEnv} ortamı`);

module.exports = {
  nodeEnv,
  port: parseInt(process.env.PORT, 10) || 3000,
  db: {
    host: process.env.DB_HOST,
    port: parseInt(process.env.DB_PORT, 10) || 5432,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    name: process.env.DB_NAME
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '7d'
  }
};
EOF

Konfigürasyon Validasyonu

Environment variable’ları doğrudan process.env.XYZ şeklinde kullanmak yerine bir validasyon katmanı eklemek gerçek dünya uygulamalarında hayat kurtarır. joi veya envalid paketi bu iş için biçilmiş kaftan:

npm install envalid
cat > config/validate-env.js << 'EOF'
const { cleanEnv, str, port, url, bool, num } = require('envalid');

const env = cleanEnv(process.env, {
  NODE_ENV: str({
    choices: ['development', 'test', 'staging', 'production'],
    default: 'development'
  }),
  PORT: port({ default: 3000 }),
  DB_HOST: str({ desc: 'PostgreSQL host adresi' }),
  DB_PORT: port({ default: 5432 }),
  DB_USER: str(),
  DB_PASSWORD: str(),
  DB_NAME: str(),
  JWT_SECRET: str({
    desc: 'JWT token imzalama anahtarı',
    docs: 'https://wiki.internal/jwt-setup'
  }),
  REDIS_URL: url({ default: 'redis://localhost:6379' }),
  EMAIL_ENABLED: bool({ default: false }),
  MAX_CONNECTIONS: num({ default: 10 })
});

// envalid, eksik veya hatalı değer varsa
// açıklayıcı hata mesajıyla process'i durdurur

module.exports = env;
EOF

Uygulama başlarken bu validasyon çalışırsa ve DB_HOST tanımlı değilse şöyle bir hata alırsın:

# envalid'in ürettiği hata çıktısı örneği
  Invalid or missing environment variables:
    DB_HOST: PostgreSQL host adresi (required)
    JWT_SECRET: JWT token imzalama anahtarı

  Refer to https://wiki.internal/jwt-setup for more info.

Bu yaklaşım sayesinde uygulaman eksik konfigürasyonla yarı çalışır hale gelmek yerine başlamayı reddeder. Production’da bir API key eksik olduğu için uygulamanın sessiz sedasız hata vermesi yerine açık bir hata mesajıyla durması çok daha tercih edilir bir durumdur.

Docker ve Container Ortamlarında Environment Variable

Uygulamanı Docker ile çalıştırıyorsan environment variable yönetimi biraz farklı işler. .env dosyasını container’ın içine kopyalamak yerine dışarıdan geçirirsin:

# docker run ile environment variable geçme
docker run -d 
  --name myapp 
  -p 3000:3000 
  -e NODE_ENV=production 
  -e DB_HOST=postgres 
  -e DB_PASSWORD=secretpassword 
  myapp:latest

# Ya da --env-file ile .env dosyasını kullan
docker run -d 
  --name myapp 
  -p 3000:3000 
  --env-file .env.production 
  myapp:latest

Docker Compose kullanıyorsan docker-compose.yml içinde şöyle tanımlarsın:

cat > docker-compose.yml << 'EOF'
version: '3.8'

services:
  app:
    build: .
    ports:
      - "${PORT:-3000}:3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=postgres
      - DB_PORT=5432
    env_file:
      - .env.production
    depends_on:
      - postgres
      - redis

  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

volumes:
  pgdata:
EOF

Burada Dockerfile içinde COPY .env . gibi bir satır kesinlikle olmamalı. Bu kritik bir güvenlik açığıdır.

PM2 ile Production Ortamında Environment Variable

Linux sunucularında Node.js uygulamalarını PM2 ile yönetiyorsan ecosystem dosyası üzerinden environment variable’ları yönetebilirsin:

cat > ecosystem.config.js << 'EOF'
module.exports = {
  apps: [
    {
      name: 'myapp',
      script: './server.js',
      instances: 'max',
      exec_mode: 'cluster',
      env: {
        NODE_ENV: 'development',
        PORT: 3000
      },
      env_production: {
        NODE_ENV: 'production',
        PORT: 8080,
        DB_HOST: 'prod-db.internal',
        DB_PORT: 5432
      },
      env_staging: {
        NODE_ENV: 'staging',
        PORT: 8081,
        DB_HOST: 'staging-db.internal'
      }
    }
  ]
};
EOF

# Production ortamıyla başlat
pm2 start ecosystem.config.js --env production

# Mevcut değişkenleri görüntüle
pm2 env 0

Dikkat: Hassas değerleri (şifreler, API key’leri) ecosystem dosyasına yazmaktan kaçın. Bu dosya da git’e gidebilir. Şifreler için sisteme önceden export yapmış olduğun değerleri process.env üzerinden okuman daha güvenlidir.

Systemd Servislerinde Environment Variable

Uygulamanı systemd servisi olarak çalıştırıyorsan environment variable’ları service dosyasında veya ayrı bir dosyada tanımlarsın:

# /etc/systemd/system/myapp.service
cat > /etc/systemd/system/myapp.service << 'EOF'
[Unit]
Description=My Node.js Application
After=network.target

[Service]
Type=simple
User=nodeapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/environment
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

# /etc/myapp/environment dosyası (600 izinleriyle koru)
mkdir -p /etc/myapp
cat > /etc/myapp/environment << 'EOF'
NODE_ENV=production
PORT=3000
DB_HOST=localhost
DB_PASSWORD=supersecretpassword
JWT_SECRET=very-long-random-string
EOF

# Sadece root okuyabilsin
chmod 600 /etc/myapp/environment
chown root:root /etc/myapp/environment

systemctl daemon-reload
systemctl enable myapp
systemctl start myapp

Gizli Değerler için Vault veya Secret Manager

Kurumsal ortamlarda environment variable’ları direkt dosyaya yazmak yerine bir secret manager kullanmak çok daha güvenlidir. HashiCorp Vault, AWS Secrets Manager veya Azure Key Vault gibi çözümler bu iş için kullanılır. Node.js uygulamasının başlangıcında bu servislerden değerleri çekip process.env‘e yazabilirsin:

cat > config/load-secrets.js << 'EOF'
const https = require('https');

async function loadSecretsFromVault() {
  // Bu örnek AWS Secrets Manager için basitleştirilmiş gösterimdir
  // Gerçek kullanımda @aws-sdk/client-secrets-manager paketi kullanılır

  if (process.env.NODE_ENV !== 'production') {
    console.log('Development ortamında yerel .env kullanılıyor');
    require('dotenv').config();
    return;
  }

  try {
    // AWS SDK ile secret çekme (pseudocode)
    const secretValue = await getSecretFromAWS(process.env.SECRET_NAME);
    const secrets = JSON.parse(secretValue);

    // Çekilen değerleri process.env'e yaz
    Object.entries(secrets).forEach(([key, value]) => {
      process.env[key] = value;
    });

    console.log('Secrets başarıyla yüklendi');
  } catch (error) {
    console.error('Secret yükleme hatası:', error.message);
    process.exit(1);
  }
}

module.exports = { loadSecretsFromVault };
EOF

Sık Yapılan Hatalar

Production ortamlarında tekrar tekrar karşılaştığım hatalar şunlar:

  • Boolean değerleri string olarak okumak: process.env.DEBUG === true her zaman false döner çünkü process.env her şeyi string verir. Doğrusu: process.env.DEBUG === 'true'
  • .env dosyasını production sunucusuna kopyalamak: Sunucuya direkt environment variable set edin
  • Tüm ortam değişkenlerini tek dosyada tutmak: Dosya büyüdükçe yönetimi zorlaşır, modüler yaklaşım benimse
  • Default değersiz zorunlu değişkenler: Zorunlu değişkenler için mutlaka başlangıçta validasyon yap
  • process.env değerlerini cache’lememek: Yüksek frekanslı işlemlerde process.env.XYZ her erişimde okunur, performans kritik yerlerde bir değişkene al

Sonuç

Environment variable yönetimi, kulağa basit gelen ama yanlış yapıldığında çok ciddi güvenlik açıklarına ve operasyonel baş ağrılarına yol açan bir konu. Temel prensipler şunlar: hassas bilgileri kaynak kodundan ayır, .env dosyalarını asla git’e commit’leme, uygulama başlarken tüm zorunlu değişkenleri validate et ve ortama göre farklı konfigürasyon dosyaları kullan.

dotenv paketi küçük ve orta ölçekli projeler için yeterli. Kurumsal ortamlarda ve mikroservis mimarilerinde Vault veya cloud secret manager’lara geçmek hem güvenlik hem de merkezi yönetim açısından doğru tercih. Systemd ile çalışan servisler için EnvironmentFile direktifi, Docker ortamları için --env-file parametresi günlük iş akışını kolaylaştırır.

Bu konuda en çok pişman olunan şey genellikle “başından düzgün kursaydım” cümlesiyle başlayan bir post-mortem toplantısında anlaşılıyor. Projenin başından itibaren bu yapıyı kurmak, sonradan refactor etmeye çalışmaktan kat kat kolay.

Bir yanıt yazın

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