Skip to content

KK - 12 Factor App

Сухой конспект и лабораторная работа по курсу 12 Factor App от KodeKloud, наполненное мыслями автора

Intro

12-факторное приложение - это методология разработки программного обеспечения, которая определяет ряд принципов для создания масштабируемых, устойчивых и легко поддерживаемых приложений в облачных средах.

Вот 12 принципов, которые описывают, как создавать 12-факторные приложения (по версии chat-gpt):

  1. Код базируется на контролируемых версиях, которые хранятся в системе контроля версий.
  2. Зависимости от сторонних библиотек и сервисов управляются явно и разделяются.
  3. Конфигурация приложения должна быть размещена в переменных окружения.
  4. Backing сервисы (базы данных, очереди сообщений и т.д.) должны рассматриваться как присоединяемые ресурсы.
  5. Приложение должно запускаться как один или несколько процессов, которые могут горизонтально масштабироваться.
  6. Логирование должно быть обеспечено как поток стандартного вывода и не должно зависеть от файловой системы.
  7. Приложение должно управлять исключениями с помощью кодов возврата.
  8. Процессы должны быть без состояний и могут быть легко перемещены или перезапущены.
  9. Разработка, тестирование и производство должны использовать одинаковую среду.
  10. Разделение запущенного приложения и конфигурации на две отдельные сущности.
  11. Администрирование приложения должно выполняться через декларативные команды.
  12. Приложение должно поддерживать возможность быстрого масштабирования на основе изменений нагрузки.

Соблюдение этих принципов поможет создать приложение, которое будет легко поддерживать, масштабировать и обновлять в облачных средах.

1. Codebase (32) - (VCS)

  • Код хранится в VCS, для того чтобы с ним можно было работать командой
  • Сущности следует дробить по мере необходимости, например приложение на микросервисы
  • ❓Как сохранять баланс в IaC-е? Например есть Х команд, для которых нужно сделать инфру: сеть, сервера
    • для каждой команды в одном tf проекте менеджить и сеть, и сервера
    • централизовано управлять сетями для всех проектов из одного места
    • если у нас добавиться еще куча ресурсов, типа bd, s3, X, то просто подробить это слегка в рамках проекта? tf-modules?

2. Dependencies (43) - (Dockerfile | .lock )

  • Никогда не следует рассчитывать на глобальные версии, все внешние зависимости должно быть жестко запинены (я думаю следует допускать легкий флекс через version-constraints, типа 0.1.X, но это риск)

3. Config (71) - (env)

  • Приложение должно хранить конфигурации в переменных окружения (но я бы сказал, что это только для оч мелких приложений годится, хороший пример флекса - traefik, он может в config.toml, env_vars, а так же флаги при запуске - у всего это чуть-чуть разный приоритет)
  • ❓ Если говорить об инфре, то как тут сохранять баланс темплейтов и переменных которые передаются в темплейты?

4. Backing Services (67) - (DB | cache)

  • Backing сервисы (базы данных, очереди сообщений и т.д.) должны рассматриваться как присоединяемые ресурсы.
  • ❓ - является ли PVC таким сервисом? 🤣
  • ❓ - если мы в infra-e, то для нас артефактом является helm chart, а как мы их будем доставлять не так важно? (helm install / argocd / flux ) - звучит оч натянуто 😉

5. Build, release, run (77) - (DevOps | CI/CD/CD -> SRE)

  • Должно быть разделение между:
    • сборкой артефакта
    • релизом (доставка + деплоймент одновременно CD/CD)
    • оперированием

6. Processes (57) - (SIGTERM | SIGKILL)

  • Приложение должно быть стейтлесс, быстро убиваться, и быть готовым обрабатывать коды выхода от CRI, чтобы корректно завершаться в облачной среде

7. Port Binding (85) - (port-mapping)

  • мало применимо в кубере, возможно следует передавать порт как ENV, на случай если мы задумаем запихать все приложения в один Pod / docker-compose service?

8. Concurrency (55) - (HPA)

  • Приложение должно уметь горизонтально масштабироваться, поэтому должно быть стейтлесс

9. Disposability (90) - (GC)

  • Приложения должны быстро подниматься и быстро убиваться

10. Dev/prod parity (95) - (tools | CI/CD)

  • dev и prod по тулам должен быть похожим для разраба

11. Logs (101)

  • Логи пишутся в stdout и коллектятся агентом, желательно в json

12. Admin Processes (107) - (declarative)

  • Админские таски типа миграции БД, должны быть декларативными, IaC

Пример: fastapi-12-factor

Пример на основе конспект от KK

Код - https://github.com/karma-git/fastapi-12-factor

1. Codebase

Список и содержимое файлов

APP_LOGLEVEL = WARNING
UVICORN_PORT = 8080
REDIS_HOST = redis-db
REDIS_PORT = 6379
---

version: '3'

services:
  api:
    container_name: "fastapi-12f"
    image: karmawow/fastapi-12f:latest
    build:
      context: ./
      dockerfile: Dockerfile
    volumes:
      - ./:/home/app
    env_file:
      - .env
    restart: always
    ports:
      # machine:container
      - "8000:8080"

  redis-db:
    image: redis
    container_name: redis
    command: redis-server /usr/local/etc/redis/redis.conf
    ports:
      - "6379:6379"
    volumes:
      - ./data:/data
      - ./redis.conf:/usr/local/etc/redis/redis.conf
FROM python:3.10.0-alpine3.14

COPY ./requirements.txt ./requirements.txt

RUN pip install --no-cache-dir -r requirements.txt

RUN addgroup --gid 10001 app \
  && adduser \
    --uid 10001 \
    --home /home/app \
    --shell /bin/ash \
    --ingroup app \
    --disabled-password \
    app

WORKDIR /home/app

USER app

COPY ./ /home/app

# default, can be overrided
ENV UVICORN_PORT 8000
EXPOSE 8000

ENTRYPOINT ["/usr/local/bin/uvicorn"]
CMD ["main:app", "--reload", "--host=0.0.0.0",  "--no-access-log"]
import os
import logging

from fastapi import FastAPI
from pydantic import BaseSettings
import redis

import json_log_formatter


class Settings(BaseSettings):
    app_name: str = "Awesome API"
    app_log_level: str = os.environ.get('APP_LOGLEVEL', 'INFO').upper()
    port: int = os.environ.get("UVICORN_PORT", 8000)
    redis_host: str = os.environ.get("REDIS_HOST", "redis-db")
    redis_port: int = os.environ.get("REDIS_PORT", 6380)

settings = Settings()

logger = logging.getLogger(__name__)
stdout = logging.StreamHandler()
stdout.setLevel(level=settings.app_log_level)
formatter = json_log_formatter.VerboseJSONFormatter()
stdout.setFormatter(formatter)
logger.addHandler(stdout)

app = FastAPI()
redis_db = redis.Redis(host=settings.redis_host, port=settings.redis_port)

@app.get("/")
async def welcomeToKodeKloud():
    try:
      redis_db.incr('visitorCount')
      visitCount = str(redis_db.get('visitorCount'), 'utf-8')
    except redis.exceptions.ConnectionError as e:
        logger.critical(e)
        return {"message": "Welcome to KODEKLOUD!"}
    else:
        logger.debug(visitCount)
        return {"message": "Welcome to KODEKLOUD!", "request_count": visitCount}

@app.get("/info")
async def info():
    return {
        "app_name": settings.app_name,
        "app_log_level": settings.app_log_level,
        "app_port": settings.port,
        "redis_host": settings.redis_host,
        "redis_port": settings.redis_port,
    }
# pip install fastapi uvicorn
# pip freeze | tee requirement.txt
anyio==3.6.2
click==8.1.3
fastapi==0.95.0
h11==0.14.0
idna==3.4
pydantic==1.10.7
sniffio==1.3.0
starlette==0.26.1
typing_extensions==4.5.0
uvicorn==0.21.1
async-timeout==4.0.2
redis==4.5.4
JSON-log-formatter==0.5.2

2. Dependencies

Храним все нестандартные python библиотеки в файле, а само приложение собираем в артефакт Dockerfile:

# pip install fastapi uvicorn
# pip freeze | tee requirement.txt
anyio==3.6.2
...
...

# Копируем файл с зависимостями
COPY ./requirements.txt ./requirements.txt

# Устанавливаем зависимости
RUN pip install --no-cache-dir -r requirements.txt

...

3. Config; Backend; Port-Binding; Logs

# Переопределяем стандартный уровень логирования
APP_LOGLEVEL = WARNING

# Переопределяем стандартный порт, на котором слушает server
# При смене, мы должны со стороны Infrastructure также его поменять
UVICORN_PORT = 8080

# Переопределяем стандартный hostname redis
# При смене, мы должны со стороны Infrastructure также его поменять
REDIS_HOST = redis-db

# Переопределяем стандартный порт, на котором слушает redis
# При смене, мы должны со стороны Infrastructure также его поменять
REDIS_PORT = 6379
...

# стандартный порт uvicorn
ENV UVICORN_PORT 8000
EXPOSE 8000

# возможно более правильный вариант, определить поведение uvicorn в main.py
# и запускать python, например с добавлением argparse
# но со стороны infra мы можем переопределить ENTRYPOINT и CMD
ENTRYPOINT ["/usr/local/bin/uvicorn"]
CMD ["main:app", "--reload", "--host=0.0.0.0",  "--no-access-log"]
---
...

services:
  api:
    ...
    # прокидываем переменные окружения из файла в рантайм
    env_file:
      - .env
    ports:
      # мапинг портов
      # docker-host:container, где порт в container должен совпадать с значением UVICORN_PORT из env
      - "8000:8080"

  # значение REDIS_HOST из env должно совпадать
  redis-db:
    # docker-host:container, где порт в container должен совпадать с значением REDIS_PORT из env
    ports:
      - "6379:6379"
import os
import logging

from fastapi import FastAPI
from pydantic import BaseSettings
import redis

import json_log_formatter

# Класс валидации и сохранения настроек через pydantic
class Settings(BaseSettings):
    app_name: str = "Awesome API"
    app_log_level: str = os.environ.get('APP_LOGLEVEL', 'INFO').upper()
    port: int = os.environ.get("UVICORN_PORT", 8000)
    redis_host: str = os.environ.get("REDIS_HOST", "redis-db")
    redis_port: int = os.environ.get("REDIS_PORT", 6380)

# Объект dict, с настройками
settings = Settings()

...

# Логирование
# Получаем логгер из файла
logger = logging.getLogger(__name__)
# создаем log хэндлер
stdout = logging.StreamHandler()
# задаем уровень логирования, TODO: добавить валидацию
stdout.setLevel(level=settings.app_log_level)
# создаем json форматтер
formatter = json_log_formatter.VerboseJSONFormatter()
# добавляем форматтер к хэндлеру
stdout.setFormatter(formatter)
# вешаем хэндлер на логгер
logger.addHandler(stdout)

# подключаем к redis с параметрами из настроек
redis_db = redis.Redis(host=settings.redis_host, port=settings.redis_port)

@app.get("/")
async def welcomeToKodeKloud():
    try:
      redis_db.incr('visitorCount')
      visitCount = str(redis_db.get('visitorCount'), 'utf-8')
    # Отлавливаем ошибки инфры и логируем их
    except redis.exceptions.ConnectionError as e:
        logger.critical(e)
        return {"message": "Welcome to KODEKLOUD!"}
    else:
        logger.debug(visitCount)
        return {"message": "Welcome to KODEKLOUD!", "request_count": visitCount}

Comments