TechLead
Lesson 18 of 25
5 min read
Cloud & Kubernetes

Cloud-Native & 12-Factor Apps

Apply the 12-Factor App methodology to build cloud-native applications that are portable, scalable, and resilient

The 12-Factor App Methodology

The 12-Factor App is a methodology for building software-as-a-service applications that are portable between execution environments, suitable for deployment on modern cloud platforms, scalable without significant changes, and minimizing divergence between development and production. These principles are foundational to cloud-native development and Kubernetes-based architectures.

The 12 Factors at a Glance

  • I. Codebase: One codebase tracked in revision control, many deploys
  • II. Dependencies: Explicitly declare and isolate dependencies
  • III. Config: Store config in the environment
  • IV. Backing Services: Treat backing services as attached resources
  • V. Build, Release, Run: Strictly separate build and run stages
  • VI. Processes: Execute the app as one or more stateless processes
  • VII. Port Binding: Export services via port binding
  • VIII. Concurrency: Scale out via the process model
  • IX. Disposability: Maximize robustness with fast startup and graceful shutdown
  • X. Dev/Prod Parity: Keep development, staging, and production as similar as possible
  • XI. Logs: Treat logs as event streams
  • XII. Admin Processes: Run admin/management tasks as one-off processes

Factor III: Config — Store Config in the Environment

Configuration that varies between deploys (database URLs, API keys, feature flags) should be stored in environment variables, not in code. In Kubernetes, this maps directly to ConfigMaps and Secrets.

// config.ts - 12-Factor compliant configuration
interface AppConfig {
  port: number;
  nodeEnv: string;
  databaseUrl: string;
  redisUrl: string;
  jwtSecret: string;
  logLevel: string;
  corsOrigin: string;
}

function loadConfig(): AppConfig {
  const required = ['DATABASE_URL', 'JWT_SECRET'];

  for (const key of required) {
    if (!process.env[key]) {
      throw new Error(`Missing required environment variable: ${key}`);
    }
  }

  return {
    port: parseInt(process.env.PORT || '3000', 10),
    nodeEnv: process.env.NODE_ENV || 'development',
    databaseUrl: process.env.DATABASE_URL!,
    redisUrl: process.env.REDIS_URL || 'redis://localhost:6379',
    jwtSecret: process.env.JWT_SECRET!,
    logLevel: process.env.LOG_LEVEL || 'info',
    corsOrigin: process.env.CORS_ORIGIN || '*',
  };
}

export const config = loadConfig();

Factor VI: Processes — Stateless Applications

Twelve-factor processes are stateless and share-nothing. Any data that needs to persist must be stored in a stateful backing service such as a database. This makes applications easy to scale horizontally in Kubernetes by simply increasing replica count.

Factor IX: Disposability — Graceful Shutdown

// server.ts - Graceful shutdown for Kubernetes
import express from 'express';

const app = express();
let isShuttingDown = false;

// Readiness probe: return 503 during shutdown
app.get('/readyz', (req, res) => {
  if (isShuttingDown) {
    return res.status(503).json({ status: 'shutting down' });
  }
  res.json({ status: 'ready' });
});

// Liveness probe
app.get('/healthz', (req, res) => {
  res.json({ status: 'alive' });
});

const server = app.listen(3000, () => {
  console.log('Server started on port 3000');
});

// Graceful shutdown handler
async function shutdown(signal: string) {
  console.log(`${signal} received. Starting graceful shutdown...`);
  isShuttingDown = true;

  // Stop accepting new connections
  server.close(async () => {
    console.log('HTTP server closed');

    // Close database connections
    // await db.end();

    // Close Redis connections
    // await redis.quit();

    console.log('Graceful shutdown complete');
    process.exit(0);
  });

  // Force exit after timeout
  setTimeout(() => {
    console.error('Forced shutdown after timeout');
    process.exit(1);
  }, 30000);
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

Factor XI: Logs — Treat Logs as Event Streams

// logger.ts - Structured JSON logging for Kubernetes
import pino from 'pino';

export const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label }),
  },
  timestamp: () => `,"timestamp":"${new Date().toISOString()}"`,
  // Never write to files — stdout only (Factor XI)
  // Kubernetes/Fluentd/Loki collects from stdout
});

// Usage:
// logger.info({ userId: 123, action: 'login' }, 'User logged in');
// logger.error({ err, requestId }, 'Database connection failed');

Cloud-Native Kubernetes Deployment

# 12-factor-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-12factor-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      terminationGracePeriodSeconds: 30  # Factor IX: Disposability
      containers:
      - name: app
        image: myregistry/my-app:1.2.3   # Factor V: Immutable build artifact
        ports:
        - containerPort: 3000             # Factor VII: Port binding
        env:                              # Factor III: Config in environment
        - name: NODE_ENV
          value: "production"
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: database-url
        envFrom:
        - configMapRef:
            name: app-config
        resources:                        # Factor VIII: Concurrency via scaling
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "256Mi"
        livenessProbe:                    # Factor IX: Health checks
          httpGet:
            path: /healthz
            port: 3000
          initialDelaySeconds: 5
        readinessProbe:
          httpGet:
            path: /readyz
            port: 3000
          initialDelaySeconds: 3

Key Takeaways

  • The 12-Factor methodology provides the blueprint for cloud-native applications
  • Externalize configuration into environment variables (ConfigMaps/Secrets in K8s)
  • Keep processes stateless for easy horizontal scaling
  • Implement graceful shutdown to handle SIGTERM from Kubernetes
  • Log to stdout/stderr as structured JSON — let the platform handle log aggregation

Continue Learning