The 12-Factor App: A Beginner's Guide to Building Better Applications

sudheer pithanisudheer pithani
9 min read

Introduction: The 12-Factor App

Note: Note: This article is tailored for DevOps Engineers and Python Enthusiasts..

If you’re diving into DevOps, you’ve probably heard about how important it is to build applications that are scalable, maintainable, and easy to deploy. The 12-Factor App is a set of best practices designed to help you do just that! These principles are especially handy for creating apps that work well in cloud environments. Let’s break down these principles, one by one, in a way that’s easy to understand and apply.

1. One Codebase

Concept: The first rule is super simple—keep all your code in one place. This “place” is usually a version control system like Git. Having a single codebase means everyone on your team is working with the same set of files, which makes collaboration and deployment much smoother.

Example: Imagine you’re building a small web app with Python and Flask. You keep all your code in one file, let’s say app.py, and store this file on your laptop. This file contains everything your app needs to handle user requests.

Why It’s Important: As your app grows and more people join your project, a single codebase ensures everyone is on the same page. No more confusion about which version of the code is the “right” one. Plus, when it’s time to deploy, you know exactly what needs to go live.

2. Explicitly Declare and Isolate Dependencies

Concept: Your app probably relies on external libraries or tools to function, right? These are called dependencies. The idea here is to clearly declare what your app depends on (like listing them in a requirements.txt file) and keep them isolated from your system’s global environment using virtual environments.

Example: Say your Flask app needs specific versions of Flask and another library like requests. You’d list these in a requirements.txt file like this:

Flask==2.0.1
requests==2.25.1

To keep these dependencies separate from other projects, you’d create a virtual environment.

Why It’s Important: Declaring and isolating dependencies ensures your app can run smoothly anywhere—whether it’s on your laptop, a colleague’s machine, or a production server. It also prevents different projects from interfering with each other by mixing up library versions.

If you don't know what virtual environments are, please read this. Otherwise, skip this part and move to the section below.

Virtual Environments (Virtual Envs) and Isolating Dependencies

Virtual Environments:

  • A virtual environment is an isolated environment for a Python project. It allows you to manage dependencies specific to your project without affecting the global Python environment on your system.

Why Use Virtual Environments?

  • Isolation: Dependencies installed in a virtual environment do not interfere with other projects or the system’s Python environment.

  • Reproducibility: Ensures that your project can be set up and run with the exact versions of packages it requires, regardless of other projects or system-wide packages.

How to Create and Use a Virtual Environment:

  1. Create a Virtual Environment:

    • Use the venv module in Python.
    python3 -m venv myenv
  • This creates a folder named myenv that contains the isolated environment.
  1. Activate the Virtual Environment:

    • On Windows:
    myenv\Scripts\activate
  • On macOS/Linux:
    source myenv/bin/activate
  • After activation, your command prompt will change to show that you’re in the virtual environment.
  1. Install Dependencies:

    • With the virtual environment active, you can install packages using pip.
    pip install package-name
  • These packages are installed only in the virtual environment, not globally.
  1. Freeze Dependencies:

    • You can create a requirements.txt file that lists all the dependencies in your virtual environment.
    pip freeze > requirements.txt
  1. Deactivate the Virtual Environment:

    • Once you're done, you can deactivate the virtual environment.
    deactivate

Isolating Dependencies:

  • Purpose: Ensures that each project uses its own set of libraries, preventing conflicts between different versions of libraries needed by different projects.

  • Tools: Virtual environments (venv), pip, requirements.txt.

3. Store Config in the Environment

Concept: Configuration settings like database URLs, API keys, or environment-specific variables should not be hardcoded in your app. Instead, store them in environment variables. This keeps your code clean and your app flexible.

Example: Instead of writing your database URL directly in your code:

DATABASE_URL = 'postgresql://user:password@localhost/dbname'

Use an environment variable:

import os
DATABASE_URL = os.getenv('DATABASE_URL')

Then, you set this environment variable outside your code.

Why It’s Important: This method makes your app easier to configure for different environments (like development, testing, or production) without changing any code. Plus, it keeps sensitive information like passwords out of your codebase, adding a layer of security.

4. Treat Backing Services as Attached Resources

Concept: Backing services are things like databases, message queues, or caching systems that your app uses. The idea is to treat these services as attached resources, meaning they should be easily replaceable and accessed through environment variables.

Example: If your Flask app uses a PostgreSQL database, you’d access it via an environment variable. If you ever need to switch to a MySQL database or another server, just change the environment variable—no need to touch your code.

Why It’s Important: This approach makes your app more flexible. You can swap out or scale up services without any headaches, keeping your app running smoothly no matter what changes behind the scenes.

5. Strictly Separate Build and Run Stages (Build, Release & Run stages)

Concept: This principle is all about keeping the build and run stages of your app separate. The build stage is where you compile or package your app, and the run stage is when it actually gets executed. Keeping these stages distinct ensures consistency.

Example: For a Flask app, the build stage might involve installing dependencies. The run stage is when you actually start the server:

# Build stage
pip install -r requirements.txt

# Run stage
flask run

Why It’s Important: By separating these stages, you ensure that your app runs the same way every time, no matter where it’s deployed. This consistency is key to avoiding “it works on my machine” issues.

6. Execute the App as One or More Stateless Processes

Concept: Your app’s processes should be stateless, meaning they don’t keep any data between requests. If your app needs to store data, use a database or another external service.

Example: In Flask, each request is handled independently. If you need to keep track of user sessions, use a database or a caching service like Redis.

Why It’s Important: Stateless processes are easier to scale and more resilient. If a process crashes, it can be restarted without losing any critical data, making your app more robust.

7. Export Services via Port Binding

Concept: Your app should be self-contained and export its services by binding to a specific port. Clients connect to this port to interact with your app.

Example: In Flask, you can run your app on a specific port:

flask run --host=0.0.0.0 --port=5000

Now, your app listens on port 5000, ready to accept requests.

Why It’s Important: Port binding makes your app more flexible and portable. It can be deployed anywhere—on your local machine, a production server, or a cloud platform—without needing to be tied to a specific environment.

What Does "Export Services via Port Binding" Mean?

Imagine your computer is a large building, and each room in the building is a different service or application. To communicate with a specific room (service), you need to know which door (port) to go through. Port binding is like assigning a door number to your application so that people (or other applications) know exactly where to go to access its services.

When we say "export services via port binding," it means that your application should be set up to listen on a specific port so that others can connect to it. Think of it like saying, "Hey, if you want to talk to me, come to door number 5000!"

Why Is This Important?

  • Self-Contained: Your application doesn’t rely on being run inside a larger system. It can stand alone, listening on its own port, ready to accept requests.

  • Easy Access: Clients (like web browsers or other programs) can easily connect to your application by knowing its port number.

8. Scale Out via the Process Model (Concurrency)

Concept: To handle more traffic, scale your app by running multiple instances of it, rather than making a single instance more powerful.

Example: If your Flask app is getting more traffic, you can run multiple instances behind a load balancer. This spreads incoming requests across all instances, allowing your app to handle more users.

This method is more cost-effective and flexible, allowing you to adjust the number of instances based on traffic. It’s a key principle for building cloud-native applications.

9. Maximize Robustness with Fast Startup and Graceful Shutdown (Disposability)

Your app should start up quickly to handle new requests and shut down gracefully to finish any in-progress tasks before terminating.

Fast startup and graceful shutdown are crucial in dynamic environments like the cloud. They ensure your app can handle changes in load and system resources without causing disruptions.

10. Keep Development, Staging, and Production as Similar as Possible

Concept: Minimize differences between development, staging, and production environments to reduce the risk of issues that only appear in production.

Example: You can use Docker to create environments that are identical across development, staging, and production. This ensures your app behaves the same way in each environment.

Why It’s Important: Keeping environments similar helps catch issues early, reducing the “it works on my machine” problem and making your app more reliable.

11. Treat Logs as Event Streams

Concept: Your app should output logs as continuous streams of event data that can be aggregated, analyzed, and stored by an external service.

Example: In Flask, you can use Python’s logging module to output logs, which can then be forwarded to an external service like Elasticsearch for analysis.

Treating logs as event streams allows you to monitor and troubleshoot your app in real-time, providing valuable insights into its behavior.

12. Run Admin/Management Tasks as One-Off Processes

Concept: Run admin tasks like database migrations or data cleanup as separate, one-off processes, rather than integrating them into your main app.

Example: If you need to perform a database migration in Flask, you might use a tool like Alembic. This task is run separately from your main app to avoid impacting its normal operations.

Keeping admin tasks separate reduces the risk of disrupting your app’s regular functions and makes it easier to automate these tasks as needed.


Conclusion

The 12-Factor App principles are a roadmap for building robust, scalable, and maintainable web applications. Whether you’re just starting out or looking to refine your development practices, these principles provide a solid foundation. By following them, you’ll be well on your way to creating apps that are not only easier to manage but also ready to thrive in today’s cloud-based world.

Sources: KodeKloud (Mumshad Mannambeth), Google, and ChatGPT


Feel free to share your thoughts in the comments! If you're a fellow beginner or just getting started with the 12-Factor AppMethodology, let's connect and learn together. 🌐

0
Subscribe to my newsletter

Read articles from sudheer pithani directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

sudheer pithani
sudheer pithani

Looking for a career in Linux, AWS & DevOps