編集制作プロダクションから転職してそろそろ3年になるが、しばらく前にオライリー・ジャパンさまよりRe:VIEW原稿の重厚な書籍『Flluent Python第2版』の制作への協力要請があり、組版と電子版の制作をしていた。
先週この出来本が届いた。圧巻の1.4kg。ヤバい。鈍器。

Pythonの入門を済ませたエンジニアが、「さらにもっと良い書き方、使い方があるのではないか?」というときに読む本。ハードな量ではあるものの、関心のあるところから読み拾っていくことで実力がついていくだろう。
ということで、制作中に構築したCIについて少し書いてみる。
自動ビルドの要件
版元のGitHubリポジトリ上にRe:VIEW原稿は揃っていて、版元のほうではすでに仮スタイルを当てた手元PDFビルドでおおまかな編集とプレビューは済んでいる状態。
ここから本格的に編集作業と本番紙面化を進めるにあたって、CIでのプレビューPDF作成が必須要件だった。
前職では大きめVPSにJenkinsを入れて済ませていたのだが、立場上離れているので自前で構築する必要がある。せっかくなのでクラウドサービスでどのくらいのコストでできるのかを試してみることにした。
CI構成にあたって要件を分解すると以下のとおり。
- インフラコストを抑える
- セキュアにする
- できるだけ速く実行する
リポジトリの更新頻度はそう高くない。人間が編集してリポジトリにpushするというタイミングに依存し、競合の可能性もほぼ無視できるだろう。GitHub Actionsを使ってビルドを呼び出すようにし、仮に何か競合(たとえば前の実行が終わってない間に後からpushが入って一時ファイルや成果物が壊れるなど)があったとしてもActionをリトライすれば十分と割り切った。
成果物であるPDFおよびビルドレポートについての置き場所については少し悩む。
- GitHubアーティファクト:プライベートリポジトリのメンバーだけが見えるという意味では最適。PDFサイズが大きくコストにつながる懸念があるのと、リリースまでやらないとアーティファクトへのアクセスが面倒。
- S3での難読URL公開:URLをどうメンバーに伝えるかわかりにくい。何かの拍子に悪意を持った者にわたったときの異常アクセスが怖い。
- VPSを使ったメンバー公開:AWSのインフラとは異なるが、万が一異常アクセスとなってもVPS固定額の範囲になる。メンバー認証を行って成果物フォルダから取得できるようにする。
ECSで構築してみる
EC2というかIaaSであればなんとでもなることがわかっているので、さして新味がない。そこで、「コスト削減に効く」とよく言われるECS上に構築してみることにした。業務の検証以外ではちゃんとECSを使ったことがなかったので、やってみたかったというだけである。
ECSの設定を手でやっていると後々泣くことになる予感があるので、最初からTerraformを使いながら進めた。
ビルドとしては次のような流れになる。
- GitHubリポジトリでpushを受けたら、以下のActionsが実行される。
- リポジトリブランチアーカイブを作成し、S3にアップロードする。
- ECSのタスクを
aws ecs run-taskで呼び出す。このタスクではリポジトリのビルドコマンドが呼び出される。S3からブランチアーカイブのダウンロード、展開、Re:VIEW→PDFのビルド、成果物のS3アップロードが行われる。 - タスクが終了するまで待機する。終了コードを保存しておく。
- S3にアップロードされた成果物をrsyncでVPSに送る。
- 保存していた終了コードを返す。

動作としてはうまくいった。しかし、しばらく運用してみて課題が見えてきた。
- 想像していたことではあったが、Re:VIEW→PDFの中核であるTeX環境およびフォントでコンテナイメージサイズが巨大(4GB)で、プロビジョニングにかなりの時間を食ってしまう。このサイズだとLambdaでも同じ問題にぶつかる。更新頻度的には起動しっぱなしにするものでもない。
- 全体に遅い。プロビジョニングも遅いし、ビルドも時間がかかる。TeXはシングルスレッドで分散処理できず、パワーで押し切るしかないからね……。
- VPCエンドポイントが予想外に高い。もっと大規模に使っているならともかく、間欠的な利用ではECRとCloudWatch用に作っているインターフェイスの課金が全体の比率として大きくなりすぎる。パケットが流れなくても存在すれば課金、VPCインターフェイスを立てるプロビジョニング時間が長いの2点の性質が厳しい。
EC2で構築してみる
ということで、結局こうなるか…というところではあるがEC2で立てることにする。Jenkinsでもよかったかもしれないけど、せっかくECSでビルドコマンドまで作っていたので、SSMで呼び出す仕組みとしてみた。インスタンスはt4g.medium。再現性のためにTerraformも継続利用している。
ビルドとしてはECSよりもシンプルになり、以下のようになる。
- GitHubリポジトリでpushを受けたら、以下のActionsが実行される。
- EC2インスタンスの状態を確認し、休止中であれば起動する。
- SSMでEC2インスタンスにアクセスし、リポジトリのビルドコマンドを呼び出す。リポジトリブランチのcheckoutとpull、Re:VIEW→PDFのビルド、成果物のS3アップロードが行われる。
- ビルドが終了するまで待機する。終了コードを保存しておく。
- S3にアップロードされた成果物をrsyncでVPSに送る。
- 保存していた終了コードを返す。

コスト抑制のためにビルドが終わったあとはEC2インスタンスを休止させたいが、制作作業をしているときは連続して何度もpushを行うのが普通だろう。起動はECSプロビジョニングよりは速いとはいえ時間がそれなりにかかるし、1つのインスタンスを使い回している都合上、起動や休止のプロセスとぶつかって何かまずいことも起きかねない。
マネージドにはEventBridgeなどを使う方法もあるようだが、インターネット老人としてビルドコマンドの冒頭に時限爆弾を仕掛けるようにした。
for job in $(atq | awk "{print \$1}"); do
atrm $job
done
echo "sudo shutdown -h now" | at now + 30 minutes
まとめ
ビルド時間としてはだいたい以下のとおり。競合起因で再実行をしたのは2回程度だった。
- ECS(目次索引なし版):9〜10分
- EC2(目次索引なし版):起動なし 7〜8分、起動済み 4分
- EC2(目次索引あり版):起動なし 9〜10分、起動済み 5〜6分
「インフラコストを抑える」「セキュアにする」「できるだけ速く実行する」の3点の観点では、手元PCでビルドよりは遅いものの、コスト的にはこのくらいだと嬉しいというところに収まった。EC2化したことでpublic subnetに置く必要は生じたが、ingressは無でSSMアクセスにしているのでセキュアに守れている。
クラウドネイティブからは遠いので、修行はまだ必要だ。