Twelve-Factor App

Twelve-Factor App

Twelve-Factor App, servis olarak çalışan modern uygulamaları daha taşınabilir, ölçeklenebilir ve operasyonel olarak yönetilebilir yapmak için kullanılan bir prensipler setidir. Özellikle SaaS, API, mikroservis ve cloud-native uygulamalarda çok işe yarar.

Bu yaklaşımın ana fikri basittir: Uygulama kodu, konfigürasyon, bağımlılıklar, runtime, loglar ve operasyonel işler birbirinden net ayrılmalıdır. Böylece aynı uygulama local, staging ve production ortamlarında daha az sürprizle çalışır.

1. Codebase

Bir uygulamanın tek bir codebase'i olmalı, farklı ortamlar ise aynı codebase'in farklı deploy'ları olarak yönetilmelidir. Production, staging ve development için ayrı ayrı kopyalanmış repository'ler zamanla birbirinden kopar.

Olmaması gereken:

payment-service-dev
payment-service-staging
payment-service-production

Olması gereken:

payment-service
  main branch
  release tags
  dev/staging/prod deploys

Repository tek olur; deploy edilen versiyon ve config ortamdan ortama değişir.

2. Dependencies

Bağımlılıklar açıkça tanımlanmalı ve izole edilmelidir. Uygulama, sunucuda tesadüfen kurulu olan bir paket veya sistem aracına gizlice güvenmemelidir.

Olmaması gereken:

# Production sunucusuna elle girip paket kurmak
ssh prod
sudo apt-get install imagemagick
npm install express
node src/server.js
// Kodun sistemde tesadufen bulunan bir binary'ye guvenmesi
import { execFile } from "node:child_process";

export function resizeImage(input, output) {
  execFile("convert", [input, "-resize", "800x800", output]);
}

Bu yaklaşımda uygulama sadece "o sunucuda" çalışır. Yeni bir instance açıldığında, CI ortamında veya container içinde aynı bağımlılıkların var olacağı garanti değildir.

Olması gereken:

{
  "scripts": {
    "start": "node src/server.js"
  },
  "dependencies": {
    "express": "4.19.2",
    "pg": "8.12.0",
    "sharp": "0.33.4"
  }
}

Python tarafında da aynı mantık geçerlidir:

fastapi==0.111.0
uvicorn==0.30.1
psycopg[binary]==3.2.1

Container kullanıyorsanız sistem bağımlılığı da image içinde açıkça tanımlanmalıdır:

FROM node:22-alpine
WORKDIR /app
RUN apk add --no-cache vips
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npm", "start"]

Kötü sinyal şudur: "Production sunucusunda zaten kurulu, package dosyasına eklemeye gerek yok." Bu cümle deploy edilebilirliği zayıflatır.

3. Config

Ortamdan ortama değişen değerler kodun içinde tutulmamalıdır. Database URL, API key, secret, feature flag ve region gibi ayarlar environment variable veya güvenli config mekanizmalarıyla verilmelidir.

Olmaması gereken:

export const config = {
  databaseUrl: "postgres://prod-user:prod-pass@10.0.0.12:5432/app",
  stripeSecretKey: "sk_live_..."
};

Olması gereken:

function requiredEnv(name) {
  const value = process.env[name];
  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`);
  }
  return value;
}

export const config = {
  port: Number(process.env.PORT || 3000),
  databaseUrl: requiredEnv("DATABASE_URL"),
  stripeSecretKey: requiredEnv("STRIPE_SECRET_KEY"),
  logLevel: process.env.LOG_LEVEL || "info"
};

Örnek .env.example dosyası:

PORT=3000
DATABASE_URL=postgres://user:pass@localhost:5432/app
STRIPE_SECRET_KEY=change-me
LOG_LEVEL=info

.env.example commit edilir, gerçek .env commit edilmez.

4. Backing Services

Database, cache, queue, object storage ve e-posta servisi gibi dış bağımlılıklar bağlı kaynaklar olarak görülmelidir. Uygulama için PostgreSQL, Redis veya S3 farkı kodun içine gömülü olmamalı; bağlantı bilgisi config ile değiştirilebilmelidir.

Olmaması gereken:

const redis = new Redis("redis://prod-redis.internal:6379");

Olması gereken:

const redis = new Redis(process.env.REDIS_URL);

Bu sayede localde Docker Redis, staging'de managed Redis, production'da başka bir Redis cluster kullanılabilir.

5. Build, Release, Run

Build, release ve run aşamaları birbirinden ayrılmalıdır.

  • Build: Kod artifact'e dönüşür.
  • Release: Artifact config ile birleşir.
  • Run: Release çalışır.

Olmaması gereken:

ssh prod
git pull
npm install
npm run build
DATABASE_URL=... node src/server.js

Olması gereken:

FROM node:22-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]

Build sırasında secret verilmez. Runtime config deployment ortamından gelir.

6. Processes

Uygulama stateless process olarak çalışmalıdır. Kalıcı veri process belleğinde veya lokal diskte tutulmamalı; veritabanı, cache veya object storage gibi backing service'lere yazılmalıdır.

Olmaması gereken:

const sessions = new Map();

app.post("/login", (req, res) => {
  const token = crypto.randomUUID();
  sessions.set(token, { userId: req.body.userId });
  res.json({ token });
});

Bu kod tek instance'ta çalışır. İkinci instance'a giden kullanıcı session'ını kaybeder.

Olması gereken:

app.post("/login", async (req, res) => {
  const token = crypto.randomUUID();

  await redis.set(
    `session:${token}`,
    JSON.stringify({ userId: req.body.userId }),
    "EX",
    60 * 60 * 24
  );

  res.json({ token });
});

Session artık process belleğinde değil, backing service üzerinde durur.

7. Port Binding

Uygulama kendi servisini bir port üzerinden dışarı açmalıdır. Container ve platform ortamlarında bu model deploy'u sadeleştirir.

Olmaması gereken:

# Uygulama kendi portunu acmiyor; sadece belirli bir sunucudaki
# web server path'ine gomulmus dosya gibi varsayiliyor.
location /payment {
  root /var/www/payment-api/public;
}
// Sabit port ve localhost binding container ortaminda sorun cikarabilir.
app.listen(3000, "127.0.0.1");

Olması gereken:

import express from "express";
import { config } from "./config.js";

const app = express();

app.get("/healthz", (req, res) => {
  res.status(200).json({ status: "ok" });
});

app.listen(config.port, "0.0.0.0", () => {
  console.log(JSON.stringify({
    level: "info",
    message: "server_started",
    port: config.port
  }));
});

Platform PORT değerini verir; uygulama o porttan servis eder.

8. Concurrency

Ölçekleme process modeliyle yapılmalıdır. Daha fazla yük geldiğinde aynı uygulamanın daha fazla process veya instance'ı çalıştırılır.

Olmaması gereken:

// Tum arka plan islerini web process'in icine gommek
app.post("/reports", async (req, res) => {
  await generateHugeReport(req.body.accountId);
  await sendReportEmail(req.body.accountId);
  res.json({ status: "sent" });
});

Bu yapı web trafiği arttığında veya rapor üretimi yavaşladığında tüm uygulamayı beraber yavaşlatır. Web, worker ve scheduler gibi process tipleri ayrılmadığı için yatay ölçekleme kaba ve pahalı hale gelir.

Olması gereken:

{
  "scripts": {
    "start:web": "node dist/web.js",
    "start:worker": "node dist/worker.js",
    "start:scheduler": "node dist/scheduler.js"
  }
}

Web process isteği alır, işi kuyruğa yazar:

app.post("/reports", async (req, res) => {
  await queue.add("generate-report", { accountId: req.body.accountId });
  res.status(202).json({ status: "queued" });
});

Worker process işi ayrı ölçeklenir:

worker.process("generate-report", async (job) => {
  const report = await generateHugeReport(job.data.accountId);
  await sendReportEmail(job.data.accountId, report);
});

Örnek Kubernetes deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-api
spec:
  replicas: 4
  selector:
    matchLabels:
      app: payment-api
  template:
    metadata:
      labels:
        app: payment-api
    spec:
      containers:
        - name: app
          image: registry.example.com/payment-api:2026.05.19
          ports:
            - containerPort: 3000
          env:
            - name: PORT
              value: "3000"
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: payment-api-secrets
                  key: database-url

Burada ölçekleme uygulama kodunu değiştirmeden replika sayısıyla yapılır.

9. Disposability

Process'ler hızlı başlayıp düzgün kapanabilmelidir. Graceful shutdown, health check, readiness probe ve hızlı startup bu prensibin pratik karşılıklarıdır.

Olmaması gereken:

// SIGTERM yakalanmiyor, readiness yok, acik request ve DB baglantilari
// platform process'i oldurdugunde yarim kalabilir.
app.listen(3000);
// Startup sirasinda agir migration calistirmak instance'in gec acilmasina
// ve deploy'un kilitlenmesine sebep olabilir.
await runAllMigrations();
await warmHugeCache();
app.listen(3000);

Olması gereken:

const server = app.listen(config.port);

let shuttingDown = false;

app.get("/readyz", (req, res) => {
  if (shuttingDown) {
    res.status(503).json({ status: "shutting_down" });
    return;
  }

  res.status(200).json({ status: "ready" });
});

async function shutdown(signal) {
  console.log(JSON.stringify({ level: "info", message: "shutdown_started", signal }));
  shuttingDown = true;

  server.close(async () => {
    await db.end();
    await redis.quit();
    console.log(JSON.stringify({ level: "info", message: "shutdown_completed" }));
    process.exit(0);
  });

  setTimeout(() => {
    console.error(JSON.stringify({ level: "error", message: "shutdown_forced" }));
    process.exit(1);
  }, 10_000).unref();
}

process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);

Bu yapı deploy sırasında request'lerin yarım kalma riskini azaltır.

10. Dev/Prod Parity

Development, staging ve production ortamları mümkün olduğunca benzer olmalıdır. Localde SQLite, staging'de MySQL, production'da PostgreSQL kullanmak çoğu zaman beklenmeyen farklar üretir.

Olmaması gereken:

Local:      SQLite + fake queue + local filesystem
Staging:    MySQL + Redis queue + NFS
Production: PostgreSQL + SQS + S3

Bu farklar yüzünden localde çalışan SQL production'da patlayabilir, dosya sistemi davranışı değişebilir veya queue semantiği farklı olduğu için duplicate mesajlar kaçırılabilir.

Olması gereken:

Local:      PostgreSQL + Redis + S3-compatible object storage
Staging:    PostgreSQL + Redis + object storage
Production: PostgreSQL + Redis + object storage

Basit bir docker-compose.yml:

services:
  app:
    build: .
    command: npm run dev
    ports:
      - "3000:3000"
    environment:
      PORT: "3000"
      DATABASE_URL: postgres://app:app@postgres:5432/app
      REDIS_URL: redis://redis:6379
    depends_on:
      - postgres
      - redis

  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    ports:
      - "5432:5432"

  redis:
    image: redis:7
    ports:
      - "6379:6379"

Local ortam production'ın birebir aynısı olmak zorunda değildir, ama aynı sınıf servisleri kullanmalıdır.

11. Logs

Loglar dosya olarak yönetilmemeli, event stream olarak stdout/stderr üzerinden dışarı akmalıdır. Log toplama, saklama, arama ve alarm üretme işi platform veya observability katmanı tarafından yapılmalıdır.

Olmaması gereken:

fs.appendFileSync("/var/log/payment-api.log", "payment completed\n");

Olması gereken:

function log(level, message, fields = {}) {
  const event = {
    level,
    message,
    service: "payment-api",
    timestamp: new Date().toISOString(),
    ...fields
  };

  const line = JSON.stringify(event);
  if (level === "error") {
    console.error(line);
  } else {
    console.log(line);
  }
}

log("info", "payment_completed", {
  orderId: "ord_123",
  paymentId: "pay_456",
  amountCents: 19900
});

Bu log platform tarafından toplanıp merkezi sisteme akar.

12. Admin Processes

Migration, veri düzeltme, cache temizleme veya tek seferlik bakım işleri uygulamayla aynı codebase ve config üzerinden çalıştırılmalıdır.

Olmaması gereken:

ssh prod-db
psql app
UPDATE users SET plan = 'pro' WHERE email LIKE '%@example.com';
# Kimin, ne zaman, hangi kod versiyonuyla calistirdigi belirsiz.
node scripts/random-fix-from-laptop.js

Bu yaklaşım audit, rollback ve tekrar edilebilirlik açısından zayıftır. Ayrıca localdeki eski kod production verisine yanlış müdahale edebilir.

Olması gereken:

{
  "scripts": {
    "start": "node dist/server.js",
    "migrate": "node dist/admin/migrate.js",
    "recalculate:balances": "node dist/admin/recalculate-balances.js"
  }
}

Örnek Kubernetes Job:

apiVersion: batch/v1
kind: Job
metadata:
  name: payment-api-migrate
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: registry.example.com/payment-api:2026.05.19
          command: ["npm", "run", "migrate"]
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: payment-api-secrets
                  key: database-url

Böylece migration production'da rastgele elle değil, izlenebilir bir release artifact'iyle çalışır.

Baştan sona küçük örnek yapı

Twelve-Factor'a daha yakın basit proje iskeleti:

payment-api/
  src/
    server.js
    config.js
    logger.js
    admin/
      migrate.js
  package.json
  Dockerfile
  docker-compose.yml
  .env.example

src/config.js:

function requiredEnv(name) {
  const value = process.env[name];
  if (!value) throw new Error(`Missing env: ${name}`);
  return value;
}

export const config = {
  port: Number(process.env.PORT || 3000),
  databaseUrl: requiredEnv("DATABASE_URL"),
  redisUrl: requiredEnv("REDIS_URL")
};

src/logger.js:

export function log(level, message, fields = {}) {
  const payload = {
    level,
    message,
    timestamp: new Date().toISOString(),
    ...fields
  };

  if (level === "error") {
    console.error(JSON.stringify(payload));
  } else {
    console.log(JSON.stringify(payload));
  }
}

src/server.js:

import express from "express";
import { config } from "./config.js";
import { log } from "./logger.js";

const app = express();
app.use(express.json());

app.get("/healthz", (req, res) => {
  res.json({ status: "ok" });
});

app.post("/payments", async (req, res) => {
  log("info", "payment_requested", { orderId: req.body.orderId });
  res.status(202).json({ status: "accepted" });
});

const server = app.listen(config.port, "0.0.0.0", () => {
  log("info", "server_started", { port: config.port });
});

process.on("SIGTERM", () => {
  log("info", "shutdown_started");
  server.close(() => {
    log("info", "shutdown_completed");
    process.exit(0);
  });
});

DevOps ekipleri için anlamı

Twelve-Factor App, sadece geliştirici prensibi değildir; deployment, operasyon, güvenlik ve gözlemlenebilirlik tarafını da düzenler. İyi uygulandığında şu problemleri azaltır:

  • Ortamlar arasında davranış farkı
  • Deploy sırasında manuel adımlar
  • Secret'ların kaynak koda sızması
  • Instance ölünce veri kaybı
  • Logların sunucu içinde kaybolması
  • Migration ve bakım işlerinin kontrolsüz yapılması
  • Yatay ölçeklemede session kaybı
  • Rollback sırasında hangi artifact'in çalıştığını bilememe

Pratik kontrol listesi

  • Uygulamanın tek bir repository ve tek bir artifact akışı var mı?
  • Tüm bağımlılıklar manifest dosyasında açık mı?
  • Secret ve ortam ayarları koddan ayrılmış mı?
  • .env.example güncel mi?
  • Build ve runtime birbirine karışıyor mu?
  • Uygulama stateless çalışabiliyor mu?
  • Instance öldüğünde kullanıcı verisi kayboluyor mu?
  • Uygulama PORT üzerinden kendini servis ediyor mu?
  • Graceful shutdown var mı?
  • Readiness ve health endpoint'leri ayrı mı?
  • Loglar stdout/stderr üzerinden structured event olarak akıyor mu?
  • Migration ve bakım işleri aynı release/config ile çalışıyor mu?

Özet

Twelve-Factor App, uygulamayı cloud ortamında daha tahmin edilebilir hale getiren bir disiplin setidir. Amaç her projeyi aynı kalıba sokmak değildir; amaç deploy edilebilirliği, taşınabilirliği ve operasyonel sadeliği artırmaktır.

Bir uygulama Twelve-Factor'a yaklaştıkça "bu sadece o sunucuda çalışıyor", "production config kodda kalmış", "instance ölünce session gitti" ve "deploy için Ahmet'in elle komut çalıştırması lazım" gibi cümleler azalır.

Kaynaklar