Azure Functions: serverless
Serverless — это не «серверов нет», а «сервер не ваш»: вы пишете одну функцию, а платформа сама решает, когда и на скольких машинах её запустить.
Azure Functions — сервис для запуска небольших кусочков кода (функций) по событию: пришёл HTTP-запрос, сработал таймер, появилось сообщение в очереди. Вы платите не за арендованное время сервера, а за фактические вызовы и потреблённые ресурсы.
Логика App Service — «арендуй машину, пусть крутится». Логика Functions — «спи, пока ничего не происходит». Если ваш код нужен раз в час на две секунды, платить за круглосуточный сервер бессмысленно. Именно эту дыру закрывают функции.
Зачем это на практике
- Фоновые задачи по расписанию: раз в ночь удалить старые файлы, собрать отчёт, разослать письма.
- Обработка событий: пользователь загрузил картинку в хранилище — функция сделала превью.
- Небольшие API и вебхуки: обработчик уведомления от платёжной системы, эндпоинт для формы обратной связи.
- Разгрузка основного приложения: тяжёлые задачи кладутся в очередь, функции разбирают её параллельно и масштабируются сами.
Триггер — сердце функции
Триггер (trigger) — событие, которое запускает функцию. У функции ровно один триггер. Дополнительно можно объявить биндинги (bindings) — декларативные «провода» к данным: входной биндинг подтягивает данные до вызова, выходной сам записывает результат, и вам не нужен SDK внутри кода.
| Триггер | Когда срабатывает | Типичное применение |
| HTTP | Пришёл HTTP-запрос | API, вебхуки |
| Timer | По расписанию (NCRONTAB) | Ночная уборка, отчёты |
| Queue / Service Bus | В очереди появилось сообщение | Асинхронная обработка задач |
| Blob | Загружен файл в хранилище | Превью, антивирус, парсинг |
| Cosmos DB | Изменилась запись в базе | Реакция на изменения данных |
Вот все три главных триггера в одном файле — это питоновская модель v2, где функции объявляются декораторами. Код использует пакет azure-functions, поэтому запустить его в браузере нельзя — он живёт в проекте функции:
import logging
import azure.functions as func
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
@app.route(route="hello")
def hello(req: func.HttpRequest) -> func.HttpResponse:
name = req.params.get("name", "мир")
return func.HttpResponse(f"Привет, {name}!", status_code=200)
# 6 полей! Первое — СЕКУНДЫ. Это «каждые 5 минут», а не «каждые 5 часов».
@app.timer_trigger(schedule="0 */5 * * * *", arg_name="timer", run_on_startup=False)
def cleanup(timer: func.TimerRequest) -> None:
if timer.past_due:
logging.warning("Таймер опоздал")
logging.info("Чищу временные файлы")
@app.queue_trigger(arg_name="msg", queue_name="orders", connection="AzureWebJobsStorage")
def process_order(msg: func.QueueMessage) -> None:
order_id = msg.get_body().decode("utf-8")
logging.info("Обрабатываю заказ %s (попытка %s)", order_id, msg.dequeue_count)Разберём по строкам. FunctionApp — контейнер, в котором регистрируются функции. http_auth_level=FUNCTION означает, что вызывать HTTP-функцию можно только с ключом в параметре ?code=...; поставьте ANONYMOUS, если эндпоинт должен быть публичным. msg.dequeue_count — счётчик попыток: если сообщение уже читали трижды, скорее всего, оно «ядовитое» и роняет обработчик.
Локальная разработка и деплой
Функции пишут и отлаживают локально через Azure Functions Core Tools — тот же самый хост, что и в облаке, только на вашей машине.
func init MyFuncProj --python
cd MyFuncProj
func start # http://localhost:7071/api/helloФункциям обязательно нужен storage account — там хост хранит служебное состояние: отметки таймеров, лизы для распределения нагрузки, очереди повторов. Без него приложение-функция просто не создастся.
az storage account create \
--name stfndemo42 \
--resource-group rg-demo \
--location westeurope \
--sku Standard_LRS
az functionapp create \
--resource-group rg-demo \
--name fn-demo-42 \
--storage-account stfndemo42 \
--consumption-plan-location westeurope \
--runtime python \
--runtime-version 3.11 \
--functions-version 4 \
--os-type Linux
func azure functionapp publish fn-demo-42Сколько это стоит
В плане Consumption счёт складывается из двух частей: числа выполнений и «GB-секунд» — потреблённой памяти, умноженной на время работы. Плюс щедрый бесплатный грант каждый месяц (не только в trial-периоде): порядка 1 000 000 выполнений и 400 000 GB-секунд. Сверх гранта — центы: около 0,20 $ за миллион выполнений.
Прикинем: функция работает 200 мс и потребляет 128 МБ. Один вызов — это 0,2 с × 0,125 ГБ = 0,025 GB-с. Даже 100 000 вызовов в месяц дают 2 500 GB-с — это внутри бесплатного гранта. Для пет-проектов и небольших вебхуков serverless фактически бесплатен.
| План | Оплата | Холодный старт |
| Consumption | За вызовы и GB-с, простой бесплатен | Есть |
| Flex Consumption | За вызовы + «всегда готовые» инстансы | Почти нет |
| Premium | За прогретые инстансы (постоянно) | Нет |
| Dedicated (App Service Plan) | За план, как у обычного веб-приложения | Нет (при Always On) |
Как это работает
Когда функция не вызывается, ваш код нигде не запущен — инстансов ноль. За событиями следит служебный компонент платформы (scale controller): он смотрит на длину очереди, поток HTTP-запросов, время таймеров и решает, сколько инстансов поднять. В Consumption их может стать до двухсот, и всё это без единой строчки настройки с вашей стороны.
Отсюда и берётся холодный старт (cold start): первому запросу после простоя приходится ждать, пока платформа выделит worker, скачает пакет с вашим кодом, поднимет контейнер, запустит рантайм (Python, .NET, Node) и импортирует модули. Это от нескольких сотен миллисекунд до нескольких секунд — на Python и Java обычно дольше, чем на .NET. После простоя (порядка 20 минут без вызовов) инстанс гасится, и всё повторяется.
Что с этим делать:
- Держать функцию лёгкой: тяжёлые импорты и создание клиентов (к БД, к HTTP) выносить на уровень модуля, чтобы они выполнялись один раз при инициализации, а не при каждом вызове.
- Не тащить в зависимости всё подряд: чем толще пакет, тем дольше он разворачивается.
- Если задержка критична (пользователь ждёт ответа) — переходить на Flex Consumption или Premium с прогретыми инстансами. Это уже фиксированная плата, но холодного старта нет.
- Если задержка не критична (обработка очереди, ночной отчёт) — просто игнорировать её. Холодный старт страшен только там, где кто-то смотрит на спиннер.
Частые ошибки
- Cron из Linux скопировали как есть. В NCRONTAB шесть полей, первое — секунды. Строка
*/5 * * * *(пять полей) означает совсем не то, что вы думаете, а0 */5 * * * *— «каждые 5 минут». - Удалили storage account. Функции перестанут работать и исчезнут из портала: там жило служебное состояние хоста.
- Функция обработала сообщение дважды. Очереди дают гарантию at-least-once, при таймауте сообщение вернётся и будет обработано снова. Пишите идемпотентный код (проверяйте, не обработан ли уже этот заказ).
- Рекурсия, которая стоит денег. Классика жанра: blob-триггер на контейнер, и сама функция кладёт результат в тот же контейнер. Каждый файл порождает новый вызов, и так по кругу. Serverless послушно масштабируется — а счёт растёт. Пишите результат в другой контейнер.
- Долгая задача в Consumption. Лимит выполнения по умолчанию 5 минут, максимум 10; а HTTP-ответа внешний шлюз ждёт около 230 секунд. Долгую работу дробите на шаги и складывайте в очередь (или берите Durable Functions).
- Создание клиента внутри тела функции. Новый HTTP-клиент или подключение к БД на каждый вызов при масштабировании до сотни инстансов исчерпывает пул соединений. Создавайте один раз, на уровне модуля.
- run_on_startup=True у таймера в проде. Функция будет срабатывать при каждой перезагрузке хоста — а он перезагружается чаще, чем вы думаете.
Итоги
- Функция = один триггер + код. Никакого сервера в вашей зоне ответственности.
- Главные триггеры: HTTP (API и вебхуки), Timer (расписание, NCRONTAB с шестью полями), Queue/Blob (события).
- Consumption платит за вызовы и GB-секунды, простой бесплатен; месячный грант ~1 млн выполнений покрывает большинство пет-проектов.
- Холодный старт — плата за то, что вы не платите за простой. Лечится Premium/Flex или просто не мешает фоновым задачам.
- Storage account обязателен: там служебное состояние хоста.
- Код должен быть идемпотентным, лёгким и коротким — платформа перезапустит его столько раз, сколько понадобится.