← 返回 Skill 列表
extension
分类: 开发与工程API Key 暂未确认

twelve-factor

十二因素应用方法论。用于云原生应用。

person作者: jakexiaohubgithub

Twelve-Factor App

The Twelve-Factor App methodology provides architectural guidelines for building portable, resilient software-as-a-service applications optimized for modern cloud platforms and container runtimes.

When to Use

  • Cloud-Native Application Architecture: Designing stateless, scalable web applications intended for Kubernetes, PaaS, or container clusters.
  • Legacy Replatforming: Modernizing monolithic legacy software into containerized, cloud-ready deployment units.
  • Zero-Downtime Rolling Deploys: Ensuring services can boot fast, survive sudden terminations, and run alongside differing versions.
  • Standardizing Microservice Conventions: Establishing uniform configuration, logging, and dependency isolation standards across teams.

Quick Start

# Dockerfile embodies dependencies, port binding, and build/run separation
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json .
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
# Dependencies
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
# Config via Env
ENV PORT=8080
# Port Binding
EXPOSE 8080
# Disposability (Process 1)
CMD ["node", "dist/main.js"]

Core Concepts

Declarative Dependencies & Port Binding

Dependencies are strictly pinned; applications self-host their web servers and bind directly to assigned ports:

// Explicit pinned dependencies in package.json
{
  "dependencies": {
    "express": "4.19.2",
    "pg": "8.12.0"
  }
}
// Self-contained port binding (no external application container like Tomcat required)
const port = process.env.PORT || 8080;
app.listen(port, () => console.log(`Server bound to port ${port}`));

Config Stored in the Environment

Strict separation of code and config; credentials, hostnames, and secrets are injected via environment variables:

// Validate environment config with Zod at startup
import { z } from "zod";

const EnvSchema = z.object({
  PORT: z.coerce.number().default(8080),
  DATABASE_URL: z.string().url(),
  REDIS_URL: z.string().url(),
  NODE_ENV: z.enum(["development", "test", "production"]).default("production"),
});

export const config = EnvSchema.parse(process.env);

Disposability & Graceful Shutdown

Processes must start fast and shut down gracefully upon receiving termination signals:

// Graceful termination handling
process.on("SIGTERM", async () => {
  console.log("SIGTERM received. Draining connections...");
  server.close(async () => {
    await dbPool.end();
    console.log("All connections closed. Exiting process.");
    process.exit(0);
  });
});

Common Patterns

Graceful Shutdown (Disposability)

Problem: Unhandled SIGTERM signals cause aborted in-flight HTTP requests and database connection leaks during rolling updates.

Solution: Intercept OS termination signals to drain traffic and close resources cleanly:

import express from "express";

const app = express();
const server = app.listen(process.env.PORT || 8080);

const shutdown = async (signal: string) => {
  console.log(`Received ${signal}, starting graceful shutdown...`);
  server.close(async () => {
    // Close DB pool, flush queues, drain connections
    await dbPool.end();
    console.log("Cleanup complete. Exiting.");
    process.exit(0);
  });

  // Force shutdown after timeout
  setTimeout(() => {
    console.error("Forcefully terminating process due to timeout.");
    process.exit(1);
  }, 10000).unref();
};

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

Best Practices

Do:

  • Treat Logs as Event Streams: Write JSON logs directly to stdout / stderr; let vector collectors (FluentBit, Promtail) ship them.
  • Execute One-Off Admin Tasks in the Same Environment: Run database migrations via one-off container jobs matching release image hashes.
  • Keep Development and Production Parity: Use Docker Compose locally to run the exact database and cache engines used in production.
  • Keep Processes Stateless: Store persistent data in external backing services (PostgreSQL, S3, Redis).

Don't:

  • Hardcode configuration or secrets in source code: Keep all credentials out of Git repositories.
  • Rely on sticky sessions: Session state must live in distributed caches (Redis) to allow effortless horizontal scaling.
  • Log to local files inside containers: Container filesystems are ephemeral and destroyed upon pod restart.

Troubleshooting

| Error | Cause | Solution | | :-------------------------------- | :------------------------------------------------------ | :---------------------------------------------------------------------------- | | Container killed with 137 (OOM) | Memory leak or unbounded container memory limit. | Set appropriate memory limits and profile process heap usage. | | EADDRINUSE | Port binding conflict or previous process did not exit. | Check running containers/processes; ensure dynamic PORT assignment. | | Missing environment variable | Config not injected during container startup. | Validate required env vars at startup using schema validation (e.g. Zod/Joi). |

References