Слово serverless («бессерверный») звучит так, будто серверов нет вообще. Это не так — серверы есть, просто вы их не видите и не управляете ими. AWS сам поднимает машину, когда нужно выполнить ваш код, и гасит её, когда работа закончена. Вы не патчите операционную систему, не настраиваете масштабирование, не платите за простаивающие инстансы — платите только за фактическое время, пока код работал.

Главный кирпич serverless в AWS — это Lambda: сервис, в который вы загружаете функцию (кусок кода), а AWS запускает её в ответ на событие. Такой подход часто называют FaaS — Function as a Service, «функция как сервис». Если вы ещё не разбирались с тем, какие вообще бывают варианты запуска кода в облаке, полезно сперва заглянуть в обзор вычислений AWS — там Lambda стоит рядом с виртуальными машинами и контейнерами. Здесь же разберём, как serverless устроен изнутри и когда его стоит брать.

Событийная модель

Обычный сервер — это программа, которая постоянно запущена и ждёт запросов. Lambda устроена наоборот: код «спит», пока не случилось событие (event) — что-то, на что функция должна отреагировать. Произошло событие — AWS запускает функцию, она отрабатывает и завершается. Нет событий — нет запущенного кода и нет счёта за работу.

Источников событий много, и в этом сила модели. Несколько типичных:

  • HTTP-запрос от пользователя — приходит через API Gateway (о нём ниже);
  • новое сообщение в очереди SQS;
  • загрузка файла в хранилище S3 (например, картинку залили — функция сделала превью);
  • запись изменилась в базе DynamoDB (через DynamoDB Streams);
  • наступило время по расписанию (раз в час, каждую ночь);
  • событие пришло на шину EventBridge от другого сервиса.

Из этой модели следует ключевое свойство — автоматическое масштабирование. Пришёл один запрос — AWS запустил одну копию функции. Пришла тысяча запросов одновременно — он запустит тысячу копий параллельно. Тысяча тут не случайная цифра, а ровно потолок по умолчанию: столько одновременных выполнений даётся на аккаунт в регионе — сразу на все ваши функции. Потолок поднимают по заявке, но знать про него надо заранее, потому что упираются в него первым делом. Вам не нужно настраивать группы автомасштабирования и балансировщики, как для обычных серверов (это подробно разобрано в статье про масштабирование и доступность). Поэтому неровная, «скачущая» нагрузка — когда то пусто, то густо — это родная стихия Lambda.

Cold start

У событийной модели есть обратная сторона, и про неё спрашивают чаще всего — это cold start («холодный старт»).

Разберём механику. Когда функция отработала, AWS не убивает её мгновенно — он держит «тёплую» среду выполнения какое-то время, чтобы следующий вызов был быстрым (это называется warm start, «тёплый старт»). Но если вызовов долго нет, среда «остывает» и удаляется. И тогда следующий вызов требует поднять всё заново: выделить машину, скачать ваш код, запустить рантайм (среду исполнения языка), выполнить инициализацию — подключения к базе, чтение настроек. Вся эта подготовка добавляется к времени ответа первого запроса. Вот эта добавка и есть cold start.

Аналогия: тёплый старт — это машина, которая уже заведена и едет. Холодный старт — это когда машина стояла всю ночь на морозе, и сперва её нужно завести и прогреть. Едет она потом одинаково, но первая поездка после простоя дольше.

Как с этим борются:

  • Provisioned concurrency — заранее держать заданное число «тёплых» функций наготове. Они не остывают, поэтому cold start пропадает — но за эту готовность приходится платить, даже когда вызовов нет.
  • Меньше зависимостей и легче пакет — чем меньше кода и библиотек нужно загрузить и проинициализировать, тем быстрее старт.
  • SnapStart — функция инициализируется один раз, AWS делает «снимок» (snapshot) готовой среды и потом запускает копии из него, минуя долгую инициализацию. Доступно это для Java, Python и .NET. Для Java денег не берут вовсе; за Python и .NET AWS просит плату за хранение снимка и за каждое восстановление из него — но это всё равно дешевле, чем круглосуточно держать прогретые экземпляры. И учтите: вместе с provisioned concurrency SnapStart не включается, средства взаимоисключающие.
  • Лёгкий рантайм — у одних языков среда исполнения стартует за миллисекунды, у других «тяжёлых» рантаймов старт самого окружения заметно дольше. Для синхронных API с жёсткими требованиями к скорости ответа это учитывают при выборе языка.

Важный практический вывод: cold start вполне терпим для фоновой и событийной обработки (никто не заметит, если разбор файла начнётся на полсекунды позже), но неприятен для синхронного API, где пользователь ждёт ответа прямо сейчас. Это один из главных критериев, брать Lambda под задачу или нет.

Лимиты Lambda

У функции есть жёсткие ограничения, которые важно знать заранее, чтобы не упереться в них в самый неподходящий момент:

  • Время выполнения. Задача обрывается на пятнадцатой минуте, а повтор начинает её заново с нуля: потолок — 15 минут (900 секунд) на один вызов, и поднять его нельзя. Долгие задачи на Lambda просто не помещаются.
  • Память — до 10 240 МБ на функцию. Причём процессорная мощность привязана к памяти: чем больше памяти выделили, тем больше vCPU функция получит. Иногда добавляют памяти не ради неё самой, а чтобы ускорить вычисления.
  • Одновременные выполнения — 1000 на аккаунт в регионе по умолчанию, на все функции сразу. Наращивается тоже не мгновенно: каждая функция прибавляет до тысячи экземпляров за десять секунд.
  • Размер ответа — 6 МБ при синхронном вызове и 1 МБ при асинхронном. Отдать из функции большой файл не выйдет: его кладут в хранилище и возвращают ссылку.
  • Размер пакета с кодом — 50 МБ архивом и 250 МБ в распакованном виде. Если библиотеки не влезают, функцию собирают образом контейнера — там потолок 10 ГБ.
  • Временный диск /tmp — от 512 МБ (по умолчанию) до 10 ГБ. Это единственное место, куда функция может что-то записать, и живёт оно ровно столько, сколько живёт экземпляр.

Эти лимиты — ещё одна причина не пытаться засунуть в Lambda всё подряд. Если задача идёт дольше 15 минут или требует постоянной тяжёлой работы — это сигнал смотреть на контейнеры или виртуальные машины.

API Gateway и Step Functions

Lambda редко живёт в одиночку. Два сервиса дополняют её чаще всего.

API Gateway — это управляемый «вход» для HTTP-запросов перед вашими функциями. Своего веб-сервера у Lambda нет, но собственный адрес в интернете она получить умеет: это называется Lambda Function URL — включается одной настройкой, доступ закрывается правами IAM или остаётся публичным. Для простого вебхука или внутренней ручки этого хватает, и шлюз не нужен вовсе.

API Gateway берут, когда нужно больше: свой домен вроде https://api.example.com/orders, проверка авторизации, ограничение частоты обращений (rate limiting), разные маршруты на разные функции. Он принимает HTTP-запрос, направляет его нужной функции и отдаёт ответ обратно клиенту. Так из набора функций получается полноценный REST API.

Простейший вызов функции через AWS CLI выглядит так:

aws lambda invoke --function-name create-order --cli-binary-format raw-in-base64-out --payload '{"item":"book"}' response.json

Флаг --cli-binary-format raw-in-base64-out здесь обязателен. В AWS CLI версии 2 (она ставится по умолчанию) параметр --payload ожидает данные в base64, и если передать сырой JSON без этого флага, команда упадёт с ошибкой Invalid base64. Флаг говорит CLI принять JSON как есть. Чтобы не дописывать его каждый раз, можно один раз выполнить aws configure set cli-binary-format raw-in-base64-out. В старой версии 1 команда работала и без флага — отсюда и старые примеры, которые на CLI 2 уже не запускаются.

Step Functions — это оркестратор многошаговых процессов. Представьте сценарий: «принять заказ, списать деньги, зарезервировать товар, отправить письмо, а если на каком-то шаге ошибка — откатить назад». Можно было бы написать это как цепочку функций, вызывающих друг друга, но такая самодельная логика быстро превращается в запутанный клубок, где непонятно, на каком шаге всё застряло.

Step Functions описывают процесс как машину состояний (state machine): явная последовательность шагов, ветвления («если успех — туда, если ошибка — сюда»), повторные попытки и паузы между шагами. Логика становится наглядной, а в консоли AWS видно, какой шаг сейчас выполняется и где что пошло не так. Описывается машина состояний декларативно, в формате JSON:

{
  "StartAt": "ChargePayment",
  "States": {
    "ChargePayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:eu-west-1:123456789012:function:charge",
      "Retry": [
        {
          "ErrorEquals": ["States.TaskFailed"],
          "IntervalSeconds": 2,
          "MaxAttempts": 3,
          "BackoffRate": 2
        }
      ],
      "Next": "ReserveItem"
    },
    "ReserveItem": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:eu-west-1:123456789012:function:reserve",
      "Catch": [
        {
          "ErrorEquals": ["States.ALL"],
          "Next": "RefundPayment"
        }
      ],
      "End": true
    },
    "RefundPayment": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:eu-west-1:123456789012:function:refund",
      "End": true
    }
  }
}

Блок Retry — это те самые повторные попытки: три раза, с паузой, которая удваивается. Блок Catch — куда уходить, если шаг всё-таки упал. И сразу важное: «откатить назад» само по себе не происходит. Шаг RefundPayment — обычная функция, которая возвращает деньги; такое возмещающее действие на каждый шаг пишут руками, а Step Functions лишь гарантирует, что до него дойдут.

Когда serverless оправдан, а когда нет

Главный навык — не «уметь писать Lambda», а понимать, где она к месту, а где навредит.

Serverless хорош, когда:

  • нагрузка событийная или «скачущая» — то пусто, то наплыв; платить за простой обидно, а пиков не предугадать;
  • задачи нерегулярные — ночная выгрузка отчёта, реакция на загрузку файла, обработка сообщений из очереди;
  • нужно склеить несколько сервисов между собой небольшим кусочком кода;
  • работа фоновая, и доли секунды задержки на cold start никого не смущают.

Serverless проигрывает, когда:

  • нагрузка постоянная и высокая — тогда предсказуемее и дешевле держать контейнеры или инстансы, которые работают всё время;
  • задачи долгие — упираются в лимит 15 минут;
  • нужен очень быстрый и стабильный ответ синхронного API, а cold start этому мешает;
  • у задачи сложное состояние, которое неудобно держать в эфемерной функции, живущей лишь время вызова.

Главная мысль: serverless — инструмент под определённый класс задач, а не замена всему остальному. «Всё на Lambda» — такая же крайность, как «ничего на Lambda». Выбирают осознанно, по характеру нагрузки.

Где это применяется

Serverless встречается повсюду, где есть событийная или нерегулярная работа: обработка загруженных пользователями файлов, реакция на сообщения в очередях, ночные задания по расписанию, склейка микросервисов, чат-боты, webhook-приёмники. Многие проекты используют Lambda не как фундамент всей системы, а точечно — там, где постоянный сервер был бы пустой тратой денег.

Типичные ошибки новичков:

  • Игнорировать cold start до того, как пользователи пожалуются на медленный первый ответ. Если строите синхронный API с требованием к скорости — закладывайте provisioned concurrency или SnapStart с самого начала, а не как пожарную меру.
  • Пытаться засунуть в Lambda долгую задачу и упереться в лимит 15 минут на середине обработки. Длинные процессы — это Step Functions, контейнеры или специальные сервисы пакетной обработки.
  • Складывать всю логику в одну гигантскую функцию. Lambda хороша маленькими, сфокусированными функциями — это и стартует быстрее, и проще тестировать.
  • Забывать про оплату за provisioned concurrency — её держат запущенной всегда, и счёт идёт даже без вызовов. Это бьёт по стоимости, если включить «на всякий случай» и забыть.

Что учить дальше: