KK - 12 Factor App
Сухой конспект и лабораторная работа по курсу 12 Factor App от KodeKloud, наполненное мыслями автора
Intro
- Презентация от KK, форк на y.disk
- https://12factor.net/ - манифест
- https://videos.itrevolution.com/watch/432442730/ -
12 factor terraform
12-факторное приложение - это методология разработки программного обеспечения, которая определяет ряд принципов для создания масштабируемых, устойчивых и легко поддерживаемых приложений в облачных средах.
Вот 12 принципов, которые описывают, как создавать 12-факторные приложения (по версии chat-gpt):
- Код базируется на контролируемых версиях, которые хранятся в системе контроля версий.
- Зависимости от сторонних библиотек и сервисов управляются явно и разделяются.
- Конфигурация приложения должна быть размещена в переменных окружения.
- Backing сервисы (базы данных, очереди сообщений и т.д.) должны рассматриваться как присоединяемые ресурсы.
- Приложение должно запускаться как один или несколько процессов, которые могут горизонтально масштабироваться.
- Логирование должно быть обеспечено как поток стандартного вывода и не должно зависеть от файловой системы.
- Приложение должно управлять исключениями с помощью кодов возврата.
- Процессы должны быть без состояний и могут быть легко перемещены или перезапущены.
- Разработка, тестирование и производство должны использовать одинаковую среду.
- Разделение запущенного приложения и конфигурации на две отдельные сущности.
- Администрирование приложения должно выполняться через декларативные команды.
- Приложение должно поддерживать возможность быстрого масштабирования на основе изменений нагрузки.
Соблюдение этих принципов поможет создать приложение, которое будет легко поддерживать, масштабировать и обновлять в облачных средах.
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
Список и содержимое файлов
---
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,
}
2. Dependencies
Храним все нестандартные python библиотеки в файле, а само приложение собираем в артефакт Dockerfile:
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}