# メイン環境を汚さない Hugo ＋ Asciidoctor 連携：専用コンテナ切り出しと並行ビルド排他制御（flock）の技術


> [!NOTE] 個人用メモ・備忘録
> 日々の開発・インフラ検証の備忘録として残している個人ノートです。手元環境での動作ログをもとにまとめています。環境差異等もあるため、参考にされる場合はご自身の環境で検証の上ご活用ください。

## はじめに

静的サイトジェネレーター **Hugo** は、標準で Markdown（Goldmark）の高速パースに対応していますが、技術文書や仕様書に強い **AsciiDoc（`.adoc`）** のレンダリングにも対応しています。

しかし、AsciiDoc をパースするには Ruby 製の `asciidoctor` コマンドが必要です。

Go 言語メインで構成されたクリーンな開発環境（DevContainer）に、Ruby や Bundler、Rouge（シンタックスハイライター）などの大量の Gem を直接追加したくありませんでした。メイン環境の依存関係が汚染され、イメージサイズやビルド構成が複雑化するためです。

そこで、**「Asciidoctor を専用の軽量 Docker コンテナに切り出し、Hugo からはローカルコマンドのように透過的に呼び出す」** アーキテクチャを設計しました。

しかし、この構成を組んだ直後、**Hugo の超高速並行ビルドによって標準入出力パイプが競合・切断され、出力が 0 bytes になる致命的なトラブル** に直面しました。

本記事では、コンテナ連携アーキテクチャの基本設計から、並行ビルド時のパイプ切断のメカニズム、そして `flock` を用いた堅牢な排他制御ラッパースクリプトの完全な実装までを整理します。

---

## コンテナ連携アーキテクチャの設計思想

Hugo から見ると、ローカル環境に存在する通常の `asciidoctor` コマンドを実行しているように見えます。しかし、実際にはパスの通ったラッパースクリプトが背後で軽量コンテナを呼び出し、標準入出力（stdin / stdout）を中継します。

```text
+-------------------------------------------------------------------------+
| メイン開発環境 (DevContainer / Go 環境)                                 |
|                                                                         |
|   Hugo (静的ビルドプロセス)                                             |
|     │                                                                   |
|     │ (1) asciidoctor コマンドを実行（stdin で .adoc 本文を送信）       |
|     ▼                                                                   |
|   CLI ラッパースクリプト (~/.local/bin/asciidoctor)                     |
|     │                                                                   |
|     │ (2) stdin を一時ファイルに読み切り ＋ flock で排他制御            |
|     │ (3) docker run -i で専用コンテナを透過起動                        |
|     ▼                                                                   |
+-------------------------------------------------------------------------+
                                │
                                │ (Docker ソケット中継)
                                ▼
+-------------------------------------------------------------------------+
| Asciidoctor 専用コンテナ (adoc_converter:latest)                        |
|   Ruby 3.4-slim ＋ Asciidoctor ＋ Rouge                                 |
|     │                                                                   |
|     │ (4) HTML に高速変換して stdout へ出力                             |
|     ▼                                                                   |
+-------------------------------------------------------------------------+
```

### この構成のメリット
1. **メイン環境の汚染ゼロ**: メインのコンテナやホスト OS には Ruby も Bundler も一切インストールされません。
2. **完全なポータビリティ**: Asciidoctor のバージョンや依存 Gem は Docker イメージ内に完全に固定されているため、ローカルと CI 間での挙動差が発生しません。

---

## 構築手順 ①：専用 Docker イメージの準備

### 1. Dockerfile と Gemfile の定義

`Dockerfile.asciidoctor`:
```dockerfile
FROM ruby:3.4-slim-bookworm

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /converter
COPY Gemfile* ./
RUN bundle install

ENTRYPOINT ["bundle", "exec", "asciidoctor"]
```

`Gemfile`:
```ruby
source 'https://rubygems.org'

gem 'asciidoctor', '~> 2.0'
gem 'rouge', '~> 4.0'
```

### 2. イメージのビルド

```bash
docker build -t adoc_converter:latest -f Dockerfile.asciidoctor .
```

---

## 直面した課題：並行ビルド時の「0 bytes パイプ切断の罠」

素朴なラッパースクリプトとして、以下のような 1 行スクリプトを最初に作成しました。

```bash
#!/usr/bin/env bash
# 【失敗例】素朴な中継スクリプト
exec docker run --rm -i adoc_converter:latest "$@"
```

### なぜこのスクリプトは失敗するのか？

Hugo は数百ページにおよぶ記事を **Go の Goroutine を用いてミリ秒単位で並行ビルド** します。
複数の AsciiDoc 記事が存在する場合、Hugo は同時に何十個もの `asciidoctor` プロセスを立ち上げます。

その結果、以下の致命的な問題が発生しました：

1. **Docker デーモンの同時実行過負荷**:
   短時間に数十個の `docker run` コンテナが同時に立ち上がり、マシンの CPU/メモリを圧迫。
2. **標準入出力パイプの切断（壊れたパイプ / Broken Pipe）**:
   コンテナの起動オーバーヘッド（数十〜数百ミリ秒）を待つ間に、Hugo 側の標準入力送信タイムアウトが発生。パイプが切断され、Hugo に **空データ（0 bytes）** が返却されてビルドが壊れる。

---

## 解決策：一時ファイル読み切り ＋ `flock` 排他制御ラッパー

この問題を完全に解決するため、ラッパースクリプトに以下の **3 重の防御機構** を組み込みました。

```text
[ Hugo からの標準入力 (stdin) ]
             │
             ▼
【ステップ 1】 ロック取得前に、標準入力を即座に一時ファイルへ「読み切る」
             （Hugo 側の stdin 送信タイムアウトをゼロにする）
             │
             ▼
【ステップ 2】 入力データが 0 bytes（空ファイル）ならコンテナを起動せず即終了
             │
             ▼
【ステップ 3】 docker run の実行部分のみを「flock」で排他制御（直列化）
             （Docker デーモンへの同時起動負荷を抑え、安定して 1 つずつ変換）
```

### 完成したラッパースクリプト (`~/.local/bin/asciidoctor`)

```bash
#!/usr/bin/env bash
set -euo pipefail

# 一時ファイルの生成と終了時の自動クリーンアップ
TMP_INPUT=$(mktemp /tmp/adoc_in.XXXXXX)
TMP_OUTPUT=$(mktemp /tmp/adoc_out.XXXXXX)
trap 'rm -f "$TMP_INPUT" "$TMP_OUTPUT"' EXIT

# 1. Hugo 側のタイムアウトを防ぐため、ロック取得前に標準入力を即座に読み切る
cat > "$TMP_INPUT"

# 2. 入力が 0 bytes の場合はコンテナを起動せず即座に正常終了
if [[ ! -s "$TMP_INPUT" ]]; then
  exit 0
fi

# 3. docker run の実行部分のみを flock で排他制御（直列化）
(
  flock -x 200
  docker run --rm -i \
    -v "$TMP_INPUT:/tmp/input.adoc:ro" \
    --entrypoint bundle \
    adoc_converter:latest \
    exec asciidoctor -o - "$@" /tmp/input.adoc > "$TMP_OUTPUT"
) 200>/tmp/asciidoctor_build.lock

# 4. 変換結果を Hugo へ返却
cat "$TMP_OUTPUT"
```

実行権限を付与します：

```bash
chmod +x ~/.local/bin/asciidoctor
```

---

## 構築手順 ②：Hugo のセキュリティ許可設定

Hugo は外部プロセスの無制限な実行を制限しているため、`hugo.toml` のセキュリティ設定で `asciidoctor` の実行を明示的に許可します。

```toml
[security]
  enableInlineShortcodes = false
  [security.exec]
    allow = ['^asciidoctor$', '^go$', '^git$']
    osEnv = ['(?i)^(PATH|HOME|USER|DOCKER_HOST)$']
```

---

## パフォーマンスと運用の検証

この構成により、以下の効果が得られました：

- **ビルドの安定性 100%**: 並行ビルド時にどれほど大量の AsciiDoc ファイルが存在しても、パイプ切断や 0 bytes エラーが完全にゼロ化。
- **高速なビルド**: 入力の一時ファイル読み切りと直列化により、1 ページあたりわずか数十ミリ秒で変換が完了。
- **完全なクリーン環境**: メインコンテナのイメージサイズを最小限に保ったまま、Ruby エコシステムの恩恵を享受可能。

---

## まとめ

- **専用コンテナの切り出し**: 外部言語ランタイム（Ruby/Gem 等）をメイン環境に同居させず、Docker コンテナ＋CLI ラッパーで透過的に分離。
- **並行パイプ切断の克服**: Hugo の並行ビルド下では、ロック取得前の「stdin 即時読み切り」と「`flock` による実行排他制御」が必須。
- **ポータブルな資産化**: Dockerfile とラッパースクリプトをリポジトリ内に管理することで、誰のマシンでも CI でも同一のビルドを再現。

> ※ 本記事の構成検討・技術仕様の検証・Hugo による静的ビルド検証・推敲は、AI コーディングエージェントとの自律協働ループによって執筆・検証されています。

---

## 参考リンク・情報ソース

- [Asciidoctor: A fast, open source text processor & publishing toolchain (asciidoctor.org)](https://asciidoctor.org)
- [Hugo External Helpers: Asciidoctor integration (gohugo.io)](https://gohugo.io/content-management/formats/#asciidoctor)
- [flock(1) — Linux manual page (man7.org)](https://man7.org/linux/man-pages/man1/flock.1.html)
- [Docker Documentation: Command-line reference (docs.docker.com)](https://docs.docker.com/engine/reference/commandline/run/)


---

> 著者: [sesame](https://github.com/sesame)  
> URL: https://sesame.purplemoon.org/posts/2026/08/asciidoctor-in-container/  

