Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Backing Services — внешние сервисы
backing_services

Backing Services — внешние сервисы

Обработка внешних сервисов (БД, кэш, очереди) как присоединённых ресурсов

Backing Services: Внешние сервисы как приложения

Обрабатывайте внешние сервисы как присоединённые ресурсы

#Суть принципа

Backing Services — четвёртый фактор 12-Factor App. Принцип гласит:

Внешние сервисы (базы данных, кэш, очереди, SMTP) должны рассматриваться как присоединённые ресурсы. Приложение не должно знать, где они находятся — только URL для подключения.

# ❌ Неправильно: приложение знает о расположении сервиса DATABASE_HOST = 'localhost' DATABASE_PORT = 5432 DATABASE_NAME = 'myapp' # ✅ Правильно: сервис как ресурс по URL import os DATABASE_URL = os.environ['DATABASE_URL'] # postgres://user:pass@db.example.com:5432/myapp

#Что такое Backing Services?

Backing Service — это любой сервис, который приложение использует как сервис:

СервисПримерыНазначение
Базы данныхPostgreSQL, MySQL, MongoDBХранение данных
КэшRedis, MemcachedКэширование, сессии
ОчередиRabbitMQ, Kafka, SQSАсинхронные задачи
ПочтаSendGrid, Mailgun, SMTPОтправка email
ХранилищаS3, GCS, Azure BlobФайлы, медиа
МониторингSentry, Datadog, New RelicЛогирование, метрики

#Ресурс, а не сервис

Ключевая идея: не различать локальные и внешние сервисы.

#Традиционный подход: различие есть

# ❌ Неправильно: разная логика для локальных и внешних сервисов # Локальная БД — специальное подключение if ENV == 'development': db = SQLite('local.db') else: # Production — PostgreSQL db = PostgreSQL(host='prod-db', port=5432) # Локальный кэш в памяти if ENV == 'development': cache = {} # Dict в памяти else: # Production — Redis cache = Redis(host='redis-cluster')

Проблемы:

  • Разное поведение в dev и prod
  • Сложно тестировать
  • Невозможно заменить один сервис на другой

#12-Factor подход: единая абстракция

# ✅ Правильно: единый интерфейс через URL import os from urllib.parse import urlparse # БД — просто ресурс по URL database_url = os.environ['DATABASE_URL'] # postgres://user:pass@localhost:5432/dev # postgres://user:pass@prod-db:5432/prod # Кэш — просто ресурс по URL cache_url = os.environ['REDIS_URL'] # redis://localhost:6379/0 # redis://redis-cluster:6379/0

Преимущества:

  • ✅ Одинаковый код для dev и prod
  • ✅ Легко заменить один сервис на другой
  • ✅ Сервис может быть локальным или удалённым — без разницы

#Подключение через URL

#Формат URL для разных сервисов

#PostgreSQL

DATABASE_URL=postgres://user:password@host:5432/database_name # Примеры: # Локально: postgres://postgres:postgres@localhost:5432/myapp # Production: postgres://app:secret@prod-db.aws:5432/myapp

#MySQL

DATABASE_URL=mysql://user:password@host:3306/database_name # Пример: DATABASE_URL=mysql://root:secret@localhost:3306/myapp

#Redis

REDIS_URL=redis://host:6379/database_number # Примеры: # Локально: redis://localhost:6379/0 # Production: redis://redis-cluster:6379/0 # С паролем: redis://:password@redis:6379/0

#MongoDB

MONGODB_URL=mongodb://user:password@host:27017/database # Пример: MONGODB_URL=mongodb://localhost:27017/myapp

#SMTP (почта)

MAIL_URL=smtp://user:password@host:587 # Примеры: # Gmail: smtp://user@gmail.com:password@smtp.gmail.com:587 # SendGrid: smtp://apikey:SENDGRID_API_KEY@smtp.sendgrid.net:587

#Парсинг URL в коде

#Python

import os from urllib.parse import urlparse # Получаем URL из окружения database_url = os.environ['DATABASE_URL'] # Парсим URL parsed = urlparse(database_url) # Извлекаем компоненты db_config = { 'host': parsed.hostname, 'port': parsed.port, 'database': parsed.path.lstrip('/'), 'username': parsed.username, 'password': parsed.password, } # Подключение import psycopg2 conn = psycopg2.connect(**db_config)

#Node.js

const { parse } = require('url'); // Получаем URL из окружения const databaseUrl = process.env.DATABASE_URL; // Парсим URL const parsed = parse(databaseUrl); // Извлекаем компоненты const config = { host: parsed.hostname, port: parsed.port, database: parsed.pathname.slice(1), user: parsed.auth.split(':')[0], password: parsed.auth.split(':')[1], }; // Подключение const { Pool } = require('pg'); const pool = new Pool(config);

#Использование готовых библиотек

# dj-database-url для Django import dj_database_url DATABASES = { 'default': dj_database_url.config( default=os.environ['DATABASE_URL'] ) } # redis-url для Redis import redis redis_client = redis.from_url(os.environ['REDIS_URL'])

#Привязка ресурсов (Resource Binding)

#Как приложение получает доступ к сервисам

#Через переменные окружения

# Явное указание URL export DATABASE_URL=postgres://user:pass@db:5432/app export REDIS_URL=redis://redis:6379/0 export MAIL_URL=smtp://user:pass@smtp:587 python app.py

#Через единый механизм привязки

В некоторых платформах (Heroku, Cloud Foundry) есть механизм resource binding:

# Heroku: привязка сервиса к приложению heroku addons:create heroku-postgresql:hobby-dev heroku addons:create rediscloud:30 # Платформа автоматически создаёт переменные окружения # DATABASE_URL, REDIS_URL доступны приложению

#Локальная разработка

# docker-compose.yml version: '3.8' services: web: build: . ports: - "5000:5000" environment: - DATABASE_URL=postgres://postgres:postgres@db:5432/app - REDIS_URL=redis://redis:6379/0 depends_on: - db - redis db: image: postgres:15 environment: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=app volumes: - postgres_data:/var/lib/postgresql/data redis: image: redis:7 volumes: - redis_data:/data volumes: postgres_data: redis_data:

Преимущества:

  • ✅ Одинаковые сервисы локально и в production
  • ✅ Не нужно вручную устанавливать PostgreSQL/Redis
  • ✅ Изоляция от других проектов

#Замена сервисов без изменения кода

#Сценарий: миграция с SQLite на PostgreSQL

# Код не меняется — меняется только DATABASE_URL # До: SQLite локально DATABASE_URL=sqlite:///local.db # После: PostgreSQL локально (как в production) DATABASE_URL=postgres://localhost:5432/myapp

#Сценарий: миграция с Redis на Memcached

# Код использует абстракцию кэша from cache import Cache cache = Cache.from_url(os.environ['CACHE_URL']) cache.set('key', 'value') # Меняется только URL: # До: CACHE_URL=redis://localhost:6379/0 # После: CACHE_URL=memcached://localhost:11211

#Сценарий: переход на managed-сервис

# До: самодельный PostgreSQL на VM DATABASE_URL=postgres://user:pass@vm-ip:5432/app # После: Amazon RDS DATABASE_URL=postgres://user:pass@rds-endpoint.amazonaws.com:5432/app # После: Heroku Postgres DATABASE_URL=postgres://user:pass@heroku-db.com:5432/app # Код не меняется!

#Антипаттерны

#❌ Жёстко закодированные хосты

# ❌ Неправильно: хосты в коде class Database: def __init__(self): if ENV == 'prod': self.host = 'prod-db.example.com' else: self.host = 'localhost'

Проблема: изменение хоста требует изменения кода.

#❌ Разная логика для dev и prod

# ❌ Неправильно: разное поведение if ENV == 'development': # Локальный файл storage = FileSystemStorage('/tmp/uploads') else: # Production: S3 storage = S3Storage('my-bucket')

Правильно: использовать один интерфейс, разная реализация через конфигурацию.

#❌ Предположение о доступности сервиса

# ❌ Неправильно: нет обработки ошибок подключения redis = Redis(host='localhost', port=6379) cache_value = redis.get('key') # Упадёт, если Redis недоступен

Правильно: обрабатывать недоступность сервиса.

# ✅ Правильно: graceful degradation try: redis = Redis.from_url(os.environ['REDIS_URL']) cache_value = redis.get('key') except ConnectionError: # Кэш недоступен — работаем без кэша cache_value = None

#❌ Несколько подключений к одному сервису

# ❌ Неправильно: каждое место создаёт своё подключение def get_user(user_id): db = create_database_connection() # Новое подключение return db.query('SELECT * FROM users WHERE id = ?', user_id) def save_user(user): db = create_database_connection() # Ещё одно подключение db.execute('INSERT INTO users VALUES (?)', user)

Правильно: использовать пул подключений.

# ✅ Правильно: пул подключений from psycopg2 import pool db_pool = pool.SimpleConnectionPool( minconn=1, maxconn=10, dsn=os.environ['DATABASE_URL'] ) def get_user(user_id): conn = db_pool.getconn() try: with conn.cursor() as cur: cur.execute('SELECT * FROM users WHERE id = %s', (user_id,)) return cur.fetchone() finally: db_pool.putconn(conn)

#Backing Services в облаке

#AWS

# RDS (PostgreSQL) DATABASE_URL=postgres://user:pass@mydb.abc123.us-east-1.rds.amazonaws.com:5432/app # ElastiCache (Redis) REDIS_URL=redis://mycache.abc123.use1.cache.amazonaws.com:6379/0 # SQS (очереди) SQS_URL=https://sqs.us-east-1.amazonaws.com/123456789/my-queue # S3 (хранилище) S3_BUCKET=my-app-bucket AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=...

#Google Cloud

# Cloud SQL (PostgreSQL) DATABASE_URL=postgres://user:pass@/app?host=/cloudsql/project:region:instance # Memorystore (Redis) REDIS_URL=redis://10.0.0.3:6379/0 # Pub/Sub (очереди) PUBSUB_PROJECT_ID=my-project PUBSUB_TOPIC=my-topic PUBSUB_SUBSCRIPTION=my-subscription # GCS (хранилище) GCS_BUCKET=my-app-bucket

#Azure

# Azure Database for PostgreSQL DATABASE_URL=postgres://user:pass@mydb.postgres.database.azure.com:5432/app # Azure Cache for Redis REDIS_URL=redis://mycache.redis.cache.windows.net:6380/0 # Service Bus (очереди) SERVICEBUS_CONNECTION_STRING=Endpoint=sb://... # Blob Storage AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;...

#Проверка соответствия принципу

Задайте себе вопросы:

  • Все ли внешние сервисы подключаются через URL?
  • Используется ли одна абстракция для dev и prod?
  • Можно ли заменить сервис без изменения кода?
  • Обрабатывается ли недоступность сервиса?
  • Используется ли пул подключений для эффективности?

#Связь с другими факторами

  • Config — URL сервисов хранятся в переменных окружения
  • Dev/Prod Parity — одинаковые сервисы в dev и prod
  • Disposability — приложение корректно закрывает подключения к сервисам

#Пример из практики

#До: тесная связанность с сервисами

# ❌ Неправильно: код зависит от конкретных сервисов # Жёстко закодированный localhost REDIS_HOST = 'localhost' REDIS_PORT = 6379 # Разная логика для dev/prod if socket.gethostname() == 'prod-server': DB_HOST = 'prod-db.example.com' else: DB_HOST = 'localhost' # Прямое создание подключений def get_redis(): return redis.Redis(host=REDIS_HOST, port=REDIS_PORT)

Проблемы:

  • Невозможно запустить в Docker без изменения кода
  • Невозможно заменить Redis на другой кэш
  • Падает при недоступности Redis

#После: сервисы как ресурсы

# ✅ Правильно: сервисы как ресурсы import os import redis from redis.exceptions import ConnectionError # URL из окружения REDIS_URL = os.environ['REDIS_URL'] # Ленивое подключение с пулом _redis_pool = None def get_redis(): global _redis_pool if _redis_pool is None: _redis_pool = redis.ConnectionPool.from_url(REDIS_URL) try: return redis.Redis(connection_pool=_redis_pool) except ConnectionError: # Graceful degradation return None # Использование redis_client = get_redis() if redis_client: cache_value = redis_client.get('key') else: # Работаем без кэша cache_value = compute_expensive_value()
# Переменные окружения для разных сред # Development REDIS_URL=redis://localhost:6379/0 # Docker Compose REDIS_URL=redis://redis:6379/0 # Production REDIS_URL=redis://redis-cluster.aws:6379/0

Результат:

  • Одинаковый код для всех сред
  • Легко заменить Redis на Memcached
  • Приложение работает при недоступности кэша

Ключевой вывод: Backing services должны быть абстрагированы как присоединённые ресурсы. Приложение получает URL сервиса из конфигурации и не знает, где он находится — локально или в облаке. Это обеспечивает переносимость и заменяемость сервисов без изменения кода.

Далее: Build, Release, Run — разделение стадий