Перейти к содержимому
getnextpdf.com

Запуск NextPDF на serverless-платформах

Нативный движок ядра NextPDF, работающий внутри процесса, — почти идеальная serverless-нагрузка. Это чистый PHP, работающий внутри вашего процессаcomposer require nextpdf/core, построить документ, получить байты. Нет внешнего двоичного файла для запуска, нет headless-браузера, нет демона, который нужно держать живым, и нет сокета к sidecar-сервису. Функция, которая строит PDF, стартует холодной, выполняет ваш PHP, возвращает байты и завершается. Это чисто ложится на AWS Lambda (через среду выполнения Bref), Google Cloud Run и AWS App Runner.

Эта страница охватывает развёртывание этого нативного движка в этих трёх средах выполнения и небольшой набор настоящих ограничений, которые они накладывают:

  • файловая система среды выполнения недолговечна: Lambda гарантирует только записываемый /tmp, тогда как контейнерные среды выполнения (Cloud Run, App Runner) имеют эфемерную, ограниченную контейнером файловую систему — в любом случае шрифты должны путешествовать внутри пакета развёртывания или образа и быть зарегистрированы в PHP (движок не читает ни одной переменной окружения с путём к шрифтам);
  • холодные старты оплачивают автозагрузку и любой прогрев шрифтов, поэтому прогревайте FontRegistry один раз на контейнер, а не на каждый вызов;
  • размер пакета, память и тайм-аут должны быть подобраны под сборку, а не под тривиальный запрос.

Эта страница только для нативного движка. Мост Chrome (writeHtmlChrome через предлагаемый пакет nextpdf/artisan) — это другая, более тяжёлая история: он вызывает headless Chromium через symfony/process, которого нет в обычном zip-пакете Lambda или в облегчённом контейнере. Запуск Chromium на Lambda означает кастомный слой с браузером и его общими библиотеками, гораздо большие пакеты и намного более долгие холодные старты — за рамками этой страницы. Голому движку всё это не нужно.

Прежде чем начать, убедитесь, что эти части на месте:

  • В вашем приложении закоммичены composer.json и composer.lock, с nextpdf/core в качестве зависимости.
  • У вас есть файлы шрифтов, которые вы намерены встроить, и у вас есть право встраивать их.
  • У вас есть инструментарий для вашей цели — CLI Bref и фреймворк serverless для Lambda, или сборка контейнера для Cloud Run / App Runner.

Почему нативный движок подходит для serverless

Заголовок раздела «Почему нативный движок подходит для serverless»

Читая прямо из пакета, nextpdf/core требует php: >=8.4 <9.0 и небольшой набор расширений PHP — ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib и ext-curl. Стандартные слои PHP Bref содержат каждое из них. Официальные контейнерные образы php:8.4 предоставляют openssl, curl и zlib из коробки, но mbstring, gd и intl не входят в набор — они требуют установки системных зависимостей и включения расширений через docker-php-ext-install (см. руководство по развёртыванию в Docker). В Bref ничего экзотического компилировать не нужно; на пути контейнера вы включаете эти три расширения при сборке образа для голого движка.

Чистоту соответствия задаёт то, чего движок не делает:

  • Нет подпроцесса для основного пути. Построение документа и вызов getPdfData() — это PHP внутри процесса от начала до конца. Зависимость symfony/process существует для опционального моста Chrome, а не для нативной отрисовки — нативная генерация PDF никогда не порождает процесс.
  • Нет постоянного состояния. Каждый вызов строит свежий документ и возвращает байты. Ничто не должно переживать запросы, кроме тёплого контейнера, который вы используете для прогрева шрифтов (ниже), но никогда не полагаетесь на него ради корректности.
  • Не нужен записываемый рабочий каталог. Движок строит PDF в памяти и возвращает его как строку; он касается диска только если вы вызываете save(). На serverless вы этого не делаете — вы возвращаете байты — поэтому отсутствие недолговечной файловой системы никогда не вредит пути сборки.

Единственное жёсткое ограничение: нет долговечной записываемой файловой системы

Заголовок раздела «Единственное жёсткое ограничение: нет долговечной записываемой файловой системы»

Файловая система развёртывания недолговечна, но модель различается по среде выполнения. AWS Lambda гарантирует только записываемый /tmp (512 МБ по умолчанию, настраивается вплоть до 10 ГБ); остальная файловая система функции доступна только для чтения. Контейнерные среды выполнения (Cloud Run, App Runner) имеют эфемерную, ограниченную контейнером записываемую файловую систему вместо модели только с /tmp — но всё, что туда записано, теряется при переработке контейнера, поэтому это рабочее пространство, а не хранилище. В любом случае предпочитайте /tmp или настроенный том для промежуточного хранения и никогда не полагайтесь на записи в путь образа приложения как на долговечное хранилище. Из этого следуют два последствия.

Никогда не вызывайте save(), ожидая долговечного вывода. NextPDF\Core\Document предоставляет и save(string $path): void, и getPdfData(): string. На serverless вы используете getPdfData() и возвращаете или загружаете байты — не относитесь к записи в каталог приложения как к постоянному хранилищу. Если вам нужно временно разместить файл (например, для multipart-загрузки в объектное хранилище), пишите под /tmp (или настроенный том) и убирайте за собой, помня, что на тёплом контейнере это рабочее пространство переживает вызовы и засчитывается в его лимит размера.

use NextPDF\Core\Document;
// Right for serverless: get the bytes, return or upload them.
$pdf = $document->getPdfData(); // string of PDF bytes, built in memory
// Avoid on serverless: save() writes to disk. On Lambda the application
// directory is read-only; on Cloud Run / App Runner it is writable but
// ephemeral (lost on container recycle). Neither is durable storage.
// $document->save('/var/task/out.pdf'); // not durable — return the bytes instead

Не устанавливайте шрифты ОС во время выполнения и не полагайтесь на автоматическое обнаружение шрифтов; упакуйте файлы шрифтов для продакшена. На Lambda файловая система только для чтения прямо блокирует apt-get install fonts-*; на контейнерной среде выполнения любая установка во время выполнения попадает на эфемерную файловую систему и теряется при следующей переработке. И это всё равно не помогло бы, потому что нативный движок не читает шрифты ОС/fontconfig — он разрешает шрифты только из файлов, которые вы регистрируете. Поэтому для продакшена файлы шрифтов должны поставляться внутри артефакта развёртывания. Если вы намеренно загружаете файлы шрифтов в /tmp или настроенный том, вы должны зарегистрировать их явно в реестре шрифтов и принять добавленные затраты на холодный старт и надёжность — это не рекомендуемый продакшен-паттерн.

Упаковка и регистрация шрифтов в пакете или образе

Заголовок раздела «Упаковка и регистрация шрифтов в пакете или образе»

Нативный движок разрешает шрифты из файлов шрифтов через NextPDF\Typography\FontRegistry, а не из fontconfig или установленных в ОС шрифтов. На serverless это не подлежит обсуждению: нет постоянной файловой системы, куда можно положить шрифты после развёртывания, поэтому они поставляются внутри пакета (zip или слой Lambda) или внутри образа (Cloud Run / App Runner).

Упакуйте ваши файлы .ttf / .otf / .ttc под каталогом в вашем проекте — resources/fonts/ — это принятое расположение — чтобы они были включены в артефакт. Затем зарегистрируйте этот каталог в PHP. Движок не читает ни одной переменной окружения с путём к шрифтам: NEXTPDF_FONTS_PATH — это значение по умолчанию ключа конфигурации fonts_path пакета nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), и оно потребляется только этой интеграцией с фреймворком, а не nextpdf/core. Голая функция должна сконструировать реестр с упакованным каталогом:

use NextPDF\Typography\FontRegistry;
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
// Register the directory the deployment artifact bundled the fonts into.
// On Lambda/Bref the code root is /var/task; adjust for your runtime.
$registry = new FontRegistry(__DIR__ . '/resources/fonts');
// (equivalently, $registry->addFontDirectory(__DIR__ . '/resources/fonts');)
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
$document = $factory->create();

Это вся serverless-забота о шрифтах. Правила именования файлов, полный API реестра и обработка недолговечной файловой системы живут на отдельной странице — не дублируйте их здесь. Прочитайте Подготовку шрифтов для нативного движка в продакшене для полного паттерна и зарегистрируйте тот же каталог, который вы упаковали. Руководство по развёртыванию в Docker охватывает эквивалентную упаковку на стороне образа для случая Cloud Run / App Runner.

Холодные старты: прогревайте FontRegistry один раз на контейнер

Заголовок раздела «Холодные старты: прогревайте FontRegistry один раз на контейнер»

Холодный старт оплачивает загрузку PHP, оптимизированный автозагрузчик Composer и любой разбор шрифтов, который запускает первая сборка. Вы не можете избежать загрузки, но вы можете вынести работу со шрифтами из горячего пути и переиспользовать её через тёплые вызовы.

Сконструируйте FontRegistry и DocumentFactory один раз, вне обработчика, чтобы они жили в течение жизни контейнера и переиспользовались при каждом тёплом вызове. По желанию вызовите warmup() с файлами шрифтов, которые вы точно будете использовать, чтобы они были разобраны во время инициализации, а не при первой отрисовке, затем lock() реестр, чтобы его разобранное состояние было заморожено и никакая мутация на каждый вызов не могла создать гонку:

use NextPDF\Typography\FontRegistry;
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
// Container-scoped, built once at cold start (module scope, not per request).
$fontsDir = __DIR__ . '/resources/fonts';
$registry = new FontRegistry($fontsDir);
// Parse the fonts you will actually use now, so the first render does not.
$registry->warmup([
$fontsDir . '/liberation/LiberationSans-Regular.ttf',
$fontsDir . '/liberation/LiberationSans-Bold.ttf',
]);
// Freeze the parsed state for the life of the warm container.
$registry->lock();
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
// Each invocation: fresh document from the shared, warm factory.
$handler = static function (array $event) use ($factory): string {
$document = $factory->create();
$document->addPage();
$document->cell(0, 10, 'Hello from serverless', newLine: true);
return $document->getPdfData();
};

Вызывайте warmup() до lock() — реестр заморожен после блокировки, поэтому прогрев после этого вызывает ошибку конфигурации. Относитесь к шрифту, который не загружается при прогреве, как к ошибке времени развёртывания, а не детали времени выполнения: проверяйте, что каждый путь шрифта, который вы намерены прогреть, действительно существует и разбирается при старте, и проваливайте развёртывание (или вашу проверку здоровья), если какой-то не разбирается, а не давайте опечатанному пути всплыть позже как недостающие глифы. Держите список прогрева на шрифтах, которые нужны типичному вызову; прогрев большого семейства, которым вы редко пользуетесь, просто удлиняет каждый холодный старт.

Bref предоставляет среду выполнения PHP для Lambda как опубликованный слой и плагин serverless.yml. Среда выполнения php-84 уже поставляет расширения, которые нужны nextpdf/core, поэтому вы развёртываете код и шрифты и направляете функцию на обработчик. Минимальный serverless.yml:

service: nextpdf-serverless
provider:
name: aws
region: us-east-1
runtime: provided.al2023
plugins:
- ./vendor/bref/bref
functions:
generate:
handler: handler.php
description: Generate a PDF with the native NextPDF engine
runtime: php-84
memorySize: 1024 # size to the build; see "Sizing" below
timeout: 30 # seconds; raise for large documents
# The Lambda filesystem is read-only except /tmp. Fonts ship in the
# package under resources/fonts and are registered in the handler.

Обработчик строит документ с тёплой, ограниченной контейнером фабрикой и возвращает байты. Для HTTP API возвращайте их в base64 с типом содержимого application/pdf, чтобы API Gateway трактовал тело как бинарное; для триггера invoke или очереди загружайте байты в объектное хранилище и возвращайте ключ:

handler.php (outline)
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
use NextPDF\Typography\FontRegistry;
// --- Cold-start: built once per container, reused across warm invocations. ---
$fontsDir = __DIR__ . '/resources/fonts';
$registry = new FontRegistry($fontsDir);
$registry->warmup([$fontsDir . '/liberation/LiberationSans-Regular.ttf']);
$registry->lock();
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
// --- Per-invocation handler. ---
return static function (array $event) use ($factory): array {
$document = $factory->create();
$document->addPage();
$document->cell(0, 10, 'Invoice', newLine: true);
// getPdfData() materializes the whole PDF in memory and returns it.
$bytes = $document->getPdfData();
return [
'statusCode' => 200,
'isBase64Encoded' => true,
'headers' => ['Content-Type' => 'application/pdf'],
'body' => base64_encode($bytes),
];
};

Проверьте, что пакет содержит здоровое окружение, прежде чем направлять на него трафик. nextpdf/core поставляет CLI, устанавливаемый в vendor/bin/nextpdf, чья команда doctor сообщает именно о тех расширениях, которые нужны движку. Запустите её один раз против того же образа или слоя среды выполнения, чтобы подтвердить, что PHP 8.4 и каждое требуемое расширение присутствуют.

Cloud Run и App Runner запускают контейнер, а не упакованную в zip функцию, поэтому сборка — это образ Docker из Контейнеризации приложения NextPDF, а не пакет Bref. Ограничения нативного движка идентичны: упакуйте шрифты в образ, зарегистрируйте упакованный каталог в PHP, запускайте без привилегий и относитесь к файловой системе как к недолговечной. В отличие от модели только с /tmp у Lambda, контейнер Cloud Run / App Runner имеет эфемерную, ограниченную контейнером записываемую файловую систему — но она сбрасывается при каждой переработке, поэтому используйте /tmp (tmpfs на Cloud Run) или настроенный том для рабочего пространства и никогда не полагайтесь на записи в путь образа приложения как на долговечное хранилище.

Отличия от Lambda операционные, а не структурные:

  • Контейнер может оставаться тёплым между запросами при настройке concurrency, поэтому прогрев FontRegistry/DocumentFactory, ограниченный контейнером, выше окупается через много запросов, а не только следующий вызов.
  • Вы обслуживаете по HTTP (SAPI FPM или встроенный PHP-сервер), а не событие invoke, поэтому возвращаете байты через ответ вашего фреймворка. Для большого документа возвращайте их как потоковый ответ — см. Потоковая передача большого сгенерированного PDF как ответ HTTP.
  • Тайм-аут запроса и память задаются на сервисе (тайм-аут / память сервиса Cloud Run; конфигурация экземпляра App Runner), а не на каждую функцию.

Всё остальное — набор расширений, регистрация шрифтов, выходной вызов getPdfData() — это тот же код, что и обработчик Lambda.

  • Размер пакета и образа. Артефакт несёт vendor/ (только продакшен — установка с --no-dev) и ваши упакованные шрифты. Шрифты доминируют: полное семейство CJK — это десятки мегабайт. Поставляйте только те шрифты, которые вы действительно отрисовываете, чтобы держать пакет Lambda под его лимитами и образ маленьким, что также сокращает холодные старты. Упакованное семейство Liberation (resources/fonts/liberation/) невелико и покрывает метрически совместимую подстановку Helvetica.
  • Память. getPdfData() строит весь документ в памяти и возвращает его как одну строку, поэтому пиковая память — это примерно размер одного готового PDF плюс рабочий набор сборки. Подбирайте память функции/контейнера под самый большой документ, который вы генерируете, а не под средний. На Lambda память также масштабирует CPU, поэтому больше памяти часто означает более быструю сборку и более дешёвый запуск, несмотря на более высокую ставку за миллисекунду — измеряйте и то, и другое. Документ из нескольких страниц комфортно умещается в 512–1024 МБ; документы с большим числом изображений или страниц требуют больше.
  • Тайм-аут. Сборка, а не передача, доминирует в бюджете запроса. Задайте тайм-аут функции выше времени сборки в худшем случае с запасом. Если документ достаточно большой, чтобы рисковать тайм-аутом, перенесите генерацию на асинхронный триггер (Lambda с очередью или job Cloud Run), который записывает результат в объектное хранилище, а не блокирует синхронный запрос.
  • Размер /tmp. Если вы что-то размещаете под /tmp, учитывайте его лимит размера и помните, что он переживает тёплые вызовы — убирайте за собой, иначе долгоживущий контейнер медленно его заполняет.
  • Нет долговечного save() в каталог приложения. Файловая система развёртывания недолговечна — каталог приложения Lambda доступен только для чтения (только /tmp принимает записи), а файловая система контейнера Cloud Run / App Runner записываема, но эфемерна. Используйте getPdfData() и возвращайте/загружайте байты; размещайте под /tmp или настроенным томом, если необходимо.
  • Не полагайтесь на автоматическое обнаружение шрифтов. Не устанавливайте шрифты ОС во время выполнения и не полагайтесь на автоматическое обнаружение шрифтов; упакуйте файлы шрифтов для продакшена. Нативный движок не читает шрифты ОС/fontconfig — он разрешает только файлы, которые вы регистрируете. Если вы намеренно загружаете файлы шрифтов в /tmp или настроенный том, вы должны зарегистрировать их явно в реестре шрифтов и принять добавленные затраты на холодный старт и надёжность. Упакуйте и зарегистрируйте файлы. См. страницу о шрифтах по ссылке выше.
  • NEXTPDF_FONTS_PATH ничего не делает для голого движка. Это значение по умолчанию конфигурации nextpdf/laravel, а не переменная, которую читает nextpdf/core. Голый обработчик Bref, который задаёт только эту переменную, не регистрирует ни одного шрифта и отрисовывает “тофу”.
  • Мост Chrome не умещается в обычную функцию. writeHtmlChrome нуждается в headless Chromium и пути подпроцесса symfony/process. Размещение Chromium на Lambda требует кастомного слоя с браузером и его библиотеками, гораздо больших пакетов и долгих холодных стартов. Нативному движку и writeHtml не нужно ничего из этого — предпочитайте их на serverless.
  • Стоимость холодного старта — это автозагрузка плюс разбор шрифтов. Используйте --optimize-autoloader при продакшен-установке и прогревайте реестр один раз на контейнер. Не прогревайте шрифты, которыми редко пользуетесь.
  • API Gateway нуждается в бинарной обработке. Возвращайте isBase64Encoded: true с Content-Type: application/pdf и настройте API трактовать application/pdf как бинарный медиатип, иначе клиент получит повреждённые байты.
  • Premium и ionCube — это более тяжёлая забота об артефакте. Сборки NextPDF Pro / Enterprise, закодированные ionCube, нуждаются в загрузчике ionCube, соответствующем точной сборке PHP в среде выполнения, которого стоковый слой Bref не включает. Это за рамками базового serverless-развёртывания core.
  • Не поставляйте dev-зависимости. Устанавливайте с --no-dev, чтобы инструментарий тестирования и анализа никогда не попадал в пакет или образ функции.
  • Проверяйте ввод перед сборкой. Сборка PDF, управляемая вводом запроса, — это вектор исчерпания памяти; отклоняйте выходящие за диапазон или слишком большие входные данные на границе до любой работы по сборке и ограничивайте concurrency, чтобы высокий трафик не множил пиковую память в сбой нехватки памяти.
  • Держите шрифты и лицензии вне публичных артефактов. Упаковывайте только те шрифты, которые вы имеете право встраивать, и никогда не запекайте файл лицензии premium в публично выкладываемый образ или слой — вместо этого предоставляйте его во время выполнения через значение окружения или менеджер секретов.
  • Наименьшие привилегии. Давайте функции/сервису только те разрешения IAM, которые ей нужны (например, доступ на запись в один выходной bucket), и запускайте контейнер без привилегий, как показывает руководство по Docker.

Это руководство не делает нормативного заявления о стандартах. Факты о платформе читаются прямо из пакета nextpdf/core: ограничение php: >=8.4 <9.0 и требуемые расширения ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib и ext-curl. Стандартный слой среды выполнения Bref PHP-8.4 содержит все шесть; официальный образ php:8.4 предоставляет openssl, curl и zlib, но mbstring, gd и intl должны быть установлены и включены при сборке образа через docker-php-ext-install (см. страницу Docker). Выходной вызов — это реальная core-поверхность NextPDF\Core\Document::getPdfData(): string (его дисковый собрат — save(string $path): void). Шрифты регистрируются через NextPDF\Typography\FontRegistry — её аргумент-каталог в конструкторе / addFontDirectory(), с warmup(array $fontFiles) и lock() для паттерна холодного старта — подключаемый через NextPDF\Core\DocumentFactory::create(). NEXTPDF_FONTS_PATH — это ключ конфигурации fonts_path пакета nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), а не переменная, которую читает nextpdf/core. Команда doctor CLI nextpdf объявлена как "bin": ["bin/nextpdf"] в пакете и устанавливается в vendor/bin/nextpdf в потребляющем приложении. Имена сред выполнения Bref и поведение AWS Lambda / Cloud Run / App Runner — это документированные возможности этих поставщиков.