Maximizing Cloud-Native Success with the Twelve-Factor App Methodology 🫡

Twelve Factor App Methodology

The Twelve-Factor App methodology is a set of best practices for building and deploying cloud-native applications. It was developed by Heroku, a cloud platform as a service (PaaS) provider, and has since been widely adopted by organizations as a guide for building cloud-native applications.

The Twelve-Factor App methodology consists of 12 principles that are designed to help developers build applications that are easy to scale, maintain, and deploy in a cloud environment.

These principles are:

1️⃣ Codebase: The codebase should be stored in version control, with a single codebase per application.

2️⃣ Dependencies: All dependencies should be declared and isolated in a separate environment, using tools such as package managers and containerization.

3️⃣ Config: Configuration should be stored in the environment, rather than in the codebase.

4️⃣ Backing services: The application should be designed to be decoupled from backing services, such as databases and message queues, so that they can be swapped out or scaled independently.

5️⃣ Build, release, run: The application should be built and deployed in a consistent and repeatable way, using a three-stage process: build, release, run.

6️⃣ Processes: The application should be designed as a set of stateless processes that can be horizontally scaled.

7️⃣ Port binding: The application should be self-contained and should expose its functionality through a set of well-defined interfaces, such as APIs or message queues.

8️⃣ Concurrency: The application should be designed to scale out horizontally, rather than relying on a single, monolithic process.

9️⃣ Disposability: The application should be designed to be started and stopped quickly, allowing for easy scaling and maintenance.

🔟 Dev/prod parity: The development, staging, and production environments should be as similar as possible, to minimize the risk of unexpected behavior in production.

1️⃣1️⃣ Logs: The application should emit structured logs, rather than writing to stdout or stderr.

1️⃣2️⃣ Admin processes: The application should have a well-defined set of admin processes for tasks such as database migrations and data backups.

By following the Twelve-Factor App methodology, developers can build cloud-native applications that are scalable, maintainable, and easy to deploy and operate in a cloud environment.

Now we’ll take a closer look at the Twelve-Factor App methodology and how it can be applied in practice, with code examples ranging from NodeJs, GoLang, Yaml, JSON , Groovy etc.

  1. Codebase:

The codebase should be stored in version control, with a single codebase per application. For example, you might use Git to store your codebase in version control:

$ git clone https://github.com/acme-inc/my-app.git
  1. Dependencies:

All dependencies should be declared and isolated in a separate environment, using tools such as package managers and containerization. For example, you might use a tool like npm to declare and manage your dependencies in a package.json file:

{
  "dependencies": {
    "express": "^4.17.1",
    "mongodb": "^3.7.4"
  }
}

You might also use a tool like Docker to containerize your application and its dependencies:

FROM node:14
COPY . .
RUN npm install
CMD ["node", "index.js"]
  1. Config:

Configuration should be stored in the environment, rather than in the codebase.

For example, you might use environment variables to store configuration values, such as database credentials or API keys:

MONGODB_URI=mongodb://user:[email protected]:27017/my-app

You can then access these environment variables in your code using a tool like dotenv:

const dotenv = require('dotenv');
dotenv.config();
const mongodbUri = process.env.MONGODB_URI;
  1. Backing services:

The application should be designed to be decoupled from backing services, such as databases and message queues, so that they can be swapped out or scaled independently. For example, you might use an abstractions layer to decouple your application from a specific database implementation:

const database = require('database-abstraction-layer');

async function createUser(user) {
  await database.create('users', user);
}
  1. Build, release, run:

The application should be built and deployed in a consistent and repeatable way, using a three-stage process: build, release, run. For example, you might use a tool like Jenkins to automate the build, release, and run stages of your deployment pipeline:

pipeline {
  agent any
  stages {
    stage('Build') {
      steps {
        sh 'npm run build'
      }
    }
    stage('Release') {
      steps {
        sh 'npm run release'
      }
    }
    stage('Run') {
      steps {
        sh 'npm start'
      }
    }
  }
}
  1. Processes:

The application should be designed as a set of stateless processes that can be horizontally scaled. For example, you might use a tool like Node.js to build your application as a set of stateless processes:

const http = require('http');

const server = http.createServer((req, res) => {
  res.end('Hello world\n');
});

server.listen(3000, () => {
  console.log('HTTP server listening on port 3000');
});
  1. Port binding:

The application should be self-contained and should expose its functionality through a set of well-defined interfaces, such as APIs or message queues. For example, you might use a tool like Express to define a set of APIs for your application:

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

app.get('/users', async (req, res) => {
  const users = await database.find('users');
  res.send(users);
});

app.listen(3000, () => {
  console.log('API server listening on port 3000');
});
  1. Concurrency:

The application should be designed to scale out horizontally, rather than relying on a single, monolithic process. For example, you might use a tool like Kubernetes to manage a fleet of replicas of your application:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app
          image: my-app:latest
          ports:
            - containerPort: 3000
  1. Disposability:

The application should be designed to be started and stopped quickly, allowing for easy scaling and maintenance.

For example, you might design your application to start up quickly and shut down gracefully, using a tool like graceful-shutdown:

const gracefulShutdown = require('graceful-shutdown');

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

gracefulShutdown(server, {
  signals: 'SIGINT SIGTERM',
  timeout: 5000,
  onShutdown: async () => {
    console.log('Shutting down server...');
    await database.close();
  },
});
  1. Dev/prod parity:

The development, staging, and production environments should be as similar as possible, to minimize the risk of unexpected behavior in production. For example, you might use a tool like Vagrant to create a local development environment that is identical to your production environment:

Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"

  config.vm.provider "virtualbox" do |vb|
    vb.memory = "1024"
  end

  config.vm.provision "shell", inline: <<-SHELL
    yum install -y epel-release
    yum install -y nodejs
  SHELL
end
  1. Logs:

The application should emit structured logs, rather than writing to stdout or stderr. For example, you might use a tool like Winston to structure your logs and write them to a file or a centralized logging service:

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
  ],
});

logger.info('Hello world');

12 .Admin processes:

The application should have a well-defined set of admin processes for tasks such as database migrations and data backups. For example, you might use a tool like Knex to define your database migrations:

exports.up = function(knex) {
  return knex.schema.createTable('users', (table) => {
    table.increments('id').primary();
    table.string('name').notNullable();
    table.string('email').unique().notNullable();
    table.timestamps();
  });
};

exports.down = function(knex) {
  return knex.schema.dropTable('users');
};

By following the Twelve-Factor App methodology, developers can build cloud-native applications that are scalable, maintainable, and easy to deploy and operate in a cloud environment.