【運営報告】ブログ自動化システムの安定化に向けた取り組み

はじめに:その自動化、本当に「自動」ですか?

「ブログの自動投稿システムを組んだけど、なぜか時々ビルドに失敗する…」 「CI/CDパイプラインがエラーで止まるたびに、原因究明に数十分も費やしている…」 「最初はシンプルだったのに、機能を追加していくうちにリポジトリがカオスになってきた…」

もしあなたが個人ブログや技術ドキュメントサイトをGitHub Actionsなどで自動化していて、このような悩みを抱えているなら、この記事はあなたのためのものです。

こんにちは。当ブログを運営している筆者です。私もかつて、まさにこの問題の渦中にいました。Markdownで記事を書いてGitHubにプッシュすれば、あとは魔法のようにサイトが更新される──そんな夢のような自動化システムを構築したはずが、いつしかそれは「時々機嫌を損ねる気難しい同居人」のような存在になっていました。依存関係のエラー、ローカルとCI環境での挙動の違い、肥大化したワークフローファイル…。これらは、自動化による恩恵を帳消しにするほどのストレスと時間的損失をもたらしていました。

この記事では、私が直面したブログ自動化システムの不安定化問題と、その根本原因を深く掘り下げ、リポジトリの再構築とビルド環境のコンテナ化というアプローチでいかにして安定稼働を実現したか、その全貌を余すところなくお伝えします。

この記事を読み終える頃には、あなたは以下の知識とテクニックを手にしているはずです。

  • 不安定なCI/CDパイプラインの根本原因を診断するための着眼点
  • モノレポとマルチレポの考え方を適用した、メンテナンス性の高いリポジトリ設計
  • Dockerを活用して「どこでも同じように動く」ビルド環境を構築する方法
  • 堅牢で再利用性の高いGitHub Actionsワークフローを設計するための具体的なベストプラクティス
  • 技術的負債と向き合い、システムを長期的に健全な状態に保つためのマインドセット

単なる対症療法ではない、根本からのシステム改善に興味のある方は、ぜひ最後までお付き合いください。

なぜブログ自動化システムは不安定になったのか? - 課題の深掘り

解決策を語る前に、まずは私のブログシステムがどのような問題を抱えていたのか、その背景と原因を詳しく見ていきましょう。問題を正しく理解することが、正しい解決策への第一歩です。

当初のシステム構成

私のブログは、多くの技術ブログで採用されているであろう、ごく一般的な構成でした。

  • コンテンツ管理: Markdownファイル
  • 静的サイトジェネレーター(SSG): Hugo
  • ソースコード管理: GitHub
  • CI/CD: GitHub Actions
  • ホスティング: GitHub Pages

この構成における自動投稿の基本的な流れは、以下の図のようになります。

graph TD
    A[記事(Markdown)をpush] --> B{GitHub Actions};
    B --> C[Hugoでビルド];
    C --> D[GitHub Pagesへデプロイ];

非常にシンプルで、最初はこれで何の問題もありませんでした。しかし、ブログ運営を続けるうちに、様々な機能を追加したくなり、システムは徐々に複雑化していきました。そして、以下の3つの大きな問題が顕在化したのです。

問題1:依存関係地獄 (Dependency Hell)

「私のローカル環境ではちゃんとビルドできるのに、なぜかGitHub Actions上では失敗する」。この現象に、あなたも見覚えがないでしょうか。これは、開発環境と実行環境の間に存在する「差異」が原因で発生します。

私の場合、具体的には以下のような問題に悩まされていました。

  • Hugoのバージョン不整合: ローカルで使っているHugoのバージョンと、GitHub ActionsのワークフローでセットアップされるHugoのバージョンが微妙に異なり、テンプレートの仕様変更などでビルドエラーが発生する。
  • Node.js依存ツールのバージョン問題: 私が使っていたHugoテーマは、CSSのトランスパイルにSass(Dart Sass)を利用しており、これはNode.jsに依存していました。ローカルのNode.js/npmバージョンとCI環境のバージョンが異なると、npm installが失敗したり、Sassのコンパイル結果が変わってしまったりしました。
  • Go Modulesの混乱: HugoはGoで書かれているため、テーマによってはGo Modules (go.mod, go.sum) を利用します。CI環境のGoのバージョンが古いと、これもまたエラーの原因となりました。

これらのバージョンをpackage.jsonやワークフローファイルで固定しようと試みましたが、複数の依存関係が絡み合うと管理が非常に煩雑になり、根本的な解決には至りませんでした。

問題2:ワークフローの肥大化と複雑化

当初はHugoでビルドしてデプロイするだけだったGitHub Actionsのワークフローファイルは、時を経て巨大な怪物へと変貌していました。

  • 記事のリンク切れをチェックするジョブ
  • 画像形式をWebPに変換し、最適化するジョブ
  • SEOのために構造化データを検証するジョブ
  • サイトマップを自動生成して検索エンジンに通知するジョブ

これらの便利な機能を次々と追加した結果、一つのYAMLファイルが数百行にも及び、誰にも全体像が把握できない状態になってしまいました。

この「モノリシック・ワークフロー」は、以下のような弊害を生み出しました。

  • 可読性の低下: ワークフロー全体の流れを追うのが困難で、新しいジョブを追加したり、既存のジョブを修正したりするのが怖い。
  • デバッグの困難さ: どこか一つのジョブが失敗すると、他の無関係なジョブまで影響を受け、デプロイ全体が停止してしまう。エラーの原因特定にも時間がかかりました。
  • 実行効率の悪化: 単に記事のタイポを一行修正しただけのpushでも、画像最適化やリンクチェックなど、時間のかかる全てのジョブが毎回実行され、CIのリソースと時間を無駄に消費していました。

問題3:モノリシックなリポジトリ構造

最大の問題は、これら全ての要素が単一のリポジトリに混在していることでした。

  • 記事のMarkdownファイル (content/)
  • Hugoのソースコードと設定ファイル (layouts/, static/, hugo.toml)
  • カスタマイズしたテーマのコード (themes/my-theme/)
  • 自動化スクリプトやワークフローファイル (.github/workflows/)
  • Node.jsの依存関係ファイル (package.json, node_modules/)

この「何でもアリ」なリポジトリ構造は、関心の分離というソフトウェア設計の基本原則に反しており、メンテナンス性を著しく低下させていました。

例えば、記事を執筆するライター(私自身ですが)は、本来Markdownファイルのことだけを気にしていれば良いはずです。しかし、この構造では、HugoのビルドロジックやCIの設定ファイルまで目に入ってしまい、誤って変更してしまうリスクがありました。

逆に、サイトのデザインを修正したい場合、テーマのCSSファイルを変更するだけなのに、記事コンテンツも一緒に管理されているため、リポジトリが肥大化し、クローンや操作が重くなっていました。

これらの問題が絡み合い、私のブログ自動化システムは、もはや「自動」と呼ぶには程遠い、手のかかる不安定な代物になってしまったのです。

解決策:リポジトリの再構築とCI/CDパイプラインの刷新

問題の根源が見えてきました。それは突き詰めると、**「環境の不一致」「関心の分離の欠如」**という、2つの古典的な課題に集約されます。

この根本原因を解消するために、私は以下の2つの大きな方針を立て、システムの全面的な再構築に踏み切りました。

  1. ビルド環境のコンテナ化 (Docker): 開発環境とCI環境の差異を撲滅し、完全な再現性を確保する。
  2. リポジトリの分割 (マルチレポ戦略): 関心事ごとにリポジトリを分割し、それぞれの責務を明確にする。

ここからは、この2つの方針に基づいた具体的な改善策を、コードや図を交えて詳細に解説していきます。

1. Dockerによるビルド環境の再現性確保

「ローカルでは動くのにCIではコケる」問題を撲滅する最も確実な方法は、ローカルとCIで全く同じ環境を使うことです。これを実現するのがDockerです。

私は、Hugo、Node.js、その他ビルドに必要なツールをすべて含んだカスタムDockerイメージを作成することにしました。これにより、ビルド環境そのものをコード(Dockerfile)として管理できるようになります。

Dockerfileの作成

以下が、私のブログ用に作成したDockerfileです。Hugoの拡張版公式イメージをベースに、Node.jsや必要なツールを追加しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# ベースイメージにはHugoの公式拡張版イメージを利用
# ARGでバージョンを外部から指定できるようにしておく
ARG HUGO_VERSION=0.125.4
FROM klakegg/hugo:${HUGO_VERSION}-ext-alpine

# ビルドに必要なツールをインストール
# alpineベースなのでapkコマンドを使用
RUN apk add --no-cache nodejs npm git

# 作業ディレクトリを設定
WORKDIR /src

# package.jsonとpackage-lock.jsonを先にコピーする
# これにより、ソースコードが変更されても、依存関係が変わらなければ
# `npm ci`のレイヤーはキャッシュが利用され、ビルドが高速化する
COPY package.json package-lock.json ./

# npm ciで依存関係を厳密にインストール
RUN npm ci

# プロジェクトのソースコード全体をコピー
COPY . .

# hugoコマンドでビルドを実行
# --minifyオプションで生成されるファイルを圧縮
RUN hugo --minify

# --- 以下はローカル開発用の設定 ---
# ポートを公開 (hugo server用)
EXPOSE 1313

# デフォルトのコンテナ起動コマンド
# ローカルでhugo serverを起動する際に使用
CMD ["hugo", "server", "-D", "--bind", "0.0.0.0", "--baseURL", "http://localhost:1313/"]

このDockerfileのポイントは、npm ciをソースコード全体のCOPYより前に実行している点です。これにより、Dockerのレイヤーキャッシュが効率的に機能し、依存関係に変更がない限り、npm ciは再実行されず、イメージビルドの時間を短縮できます。

ローカル開発環境での利用

ローカルでの執筆・プレビュー時にもこのDockerイメージを使うことで、環境の差異を完全になくします。そのためにdocker-compose.ymlを用意します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
version: '3.8'
services:
  hugo:
    # カレントディレクトリのDockerfileを使ってイメージをビルド
    build:
      context: .
      dockerfile: Dockerfile
    # ホストマシンのカレントディレクトリをコンテナの/srcにマウント
    # これにより、ローカルでファイルを編集すると即座にコンテナ内に反映される
    volumes:
      - .:/src
    # ホストの1313番ポートをコンテナの1313番ポートにフォワーディング
    ports:
      - "1313:1313"

この設定により、ターミナルでdocker-compose upというコマンドを一つ実行するだけで、ローカルにHugoやNode.jsがインストールされていなくても、誰でも全く同じ開発環境を起動できるようになりました。

これで、「環境の不一致」という最大の問題は解決です。

2. マルチレポ戦略による関心の分離

次に、「関心の分離の欠如」という問題に取り組みます。私は、巨大化したモノリシックなリポジトリを、責務に基づいて以下の3つのリポジトリに分割しました。

  1. blog-contents: 記事のMarkdownファイルのみを管理するリポジトリ。執筆活動の拠点です。
  2. blog-theme: Hugoのテーマ、サイト設定 (hugo.toml)、ビルド設定 (Dockerfile, package.jsonなど)、GitHub Actionsワークフローなど、サイトの骨格とビルドロジックを管理するリポジトリ。
  3. blog-deploy: Hugoが生成した静的ファイル(HTML, CSS, JS)を格納し、GitHub Pagesで公開するためのリポジトリ。

この新しい構成を図にすると、以下のようになります。

graph TD
    subgraph "執筆リポジトリ (blog-contents)"
        A[記事(Markdown)をpush]
    end

    subgraph "テーマ/ビルド リポジトリ (blog-theme)"
        B[Dockerfile]
        C[hugo.toml]
        D[テーマファイル]
        W[.github/workflows/deploy.yml]
    end

    subgraph "デプロイリポジトリ (blog-deploy)"
        F[生成されたHTML/CSS/JS]
    end

    A -- トリガー --> E{GitHub Actions};
    E -- ワークフロー定義の読み込み --> W;
    E -- 1. blog-themeをチェックアウト --> D;
    E -- 2. blog-contentsをチェックアウト --> A_content[Markdown];
    E -- 3. Dockerイメージビルド --> B;
    E -- 4. Hugoビルド実行 --> G[ビルド処理];
    G -- 5. 生成物をpush --> F;

    F --> H[GitHub Pages];

この構成の肝は、GitHub Actionsのワークフローです。blog-contentsリポジトリへのpushをトリガーに、blog-themeリポジトリで定義されたワークフローが実行されます。ワークフローは、blog-theme自身とblog-contentsの両方をチェックアウトし、blog-theme内のDockerfileを使ってビルドを実行。最後に、生成物をblog-deployリポジトリにプッシュします。

新しいGitHub Actionsワークフロー

以下は、この新しい構成を実現するためのblog-themeリポジトリに配置するワークフローファイル (.github/workflows/deploy.yml) の例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
name: Build and Deploy Blog

on:
  # blog-contentsリポジトリのmainブランチへのpushをトリガーにする
  # repository_dispatchイベントを利用
  repository_dispatch:
    types: [build-blog]
  # 手動実行も可能にする
  workflow_dispatch:

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      # Step 1: テーマとビルドロジックのリポジトリをチェックアウト
      - name: Checkout theme and build repository
        uses: actions/checkout@v4
        with:
          repository: your-username/blog-theme
          path: blog-theme

      # Step 2: 記事コンテンツのリポジトリをチェックアウト
      - name: Checkout contents repository
        uses: actions/checkout@v4
        with:
          repository: your-username/blog-contents
          path: blog-contents

      # Step 3: Dockerイメージをビルド
      # キャッシュを活用してビルドを高速化
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Build Docker image
        uses: docker/build-push-action@v5
        with:
          context: ./blog-theme
          load: true
          tags: blog-builder:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

      # Step 4: Dockerコンテナ内でHugoビルドを実行
      - name: Build Hugo site
        run: |
          docker run --rm \
            -v $(pwd)/blog-contents:/src/content \
            -v $(pwd)/public:/src/public \
            blog-builder:latest

      # Step 5: 生成された静的ファイルをデプロイリポジトリにプッシュ
      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          # デプロイ先のリポジトリとブランチを指定
          external_repository: your-username/blog-deploy
          publish_branch: main
          # デプロイディレクトリを指定
          publish_dir: ./public
          # コミットユーザー情報を設定
          user_name: 'github-actions[bot]'
          user_email: 'github-actions[bot]@users.noreply.github.com'

注: repository_dispatchを外部リポジトリからトリガーするには、blog-contentsリポジトリ側でPAT(Personal Access Token)を使いAPIを叩くワークフローが別途必要になります。よりシンプルな構成としては、blog-themeリポジトリにon.pushトリガーを設定し、blog-contentsをGit Submoduleとして管理する方法も考えられます。

このリポジトリ分割により、関心事が明確に分離され、それぞれの役割に集中できる環境が整いました。

改善によるメリットとデメリット

この大掛かりな再構築によって、何が良くなり、そしてどのような新たなトレードオフが生まれたのでしょうか。

メリット

  1. 絶大な安定性と再現性: Docker化により、「私のマシンでは動くのに」問題は完全に過去のものとなりました。CIは常に期待通りに動作し、ビルド失敗に悩まされる時間はゼロに近くなりました。
  2. 劇的に向上したメンテナンス性:
    • 執筆者: Markdownを書くことだけに集中できます。ビルドの仕組みを意識する必要はありません。
    • 開発者: サイトのデザイン変更や機能追加はblog-themeリポジトリで完結します。記事コンテンツに影響を与える心配なく、大胆なリファクタリングも可能です。
  3. 効率化されたCI/CDパイプライン: Dockerレイヤーキャッシュの活用により、依存関係に変更がない限りビルドは高速です。また、記事の更新とテーマの更新で関心が分離されているため、不要なジョブが実行されることもありません。
  4. 将来の拡張性 (スケーラビリティ): もし将来、HugoからAstroやNext.jsのような別のSSGに乗り換えたくなったとしても、blog-contentsリポジトリには一切手を加える必要がありません。blog-themeリポジトリを新しい技術スタックで再構築するだけで移行が完了します。これは非常に大きな利点です。

デメリット

  1. 構成の複雑化: リポジトリが1つから3つに増え、全体のアーキテクチャを理解するための初期学習コストは確実に上がりました。新しいメンバーが参加した際の説明も、以前より少し手間がかかります。
  2. 初期セットアップの手間: Dockerfileやdocker-compose.yml、リポジトリ間を連携させるためのGitHub Actionsワークフローの初期設定は、それなりに知識と時間を要します。
  3. リソース消費: Dockerイメージをビルド・保存するために、GitHub Actionsの実行時間や、GHCR (GitHub Container Registry) などのストレージ容量を消費します。小規模なブログでは過剰装備と感じるかもしれません。

これらのデメリットは存在しますが、長期的な運用を見据えた場合、得られる安定性とメンテナンス性のメリットはそれを遥かに上回ると私は確信しています。

現場で使える実践的なTips

今回の再構築を通して得られた、さらに一歩進んだ知見やテクニックをいくつかご紹介します。

Tip 1: Dependabotによる依存関係の自動更新

安定性を追求するあまり、各種ライブラリのバージョンを塩漬けにしてしまうのは良いプラクティスではありません。セキュリティ脆弱性に対応するためにも、依存関係は定期的に更新すべきです。 blog-themeリポジトリにDependabotを設定しましょう。以下の.github/dependabot.ymlをリポジトリに追加するだけで、Dockerfile内のHugoバージョンや、package.json内のnpmパッケージが古くなった場合に、自動で更新のプルリクエストを作成してくれます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
version: 2
updates:
  # Dockerfileのバージョンをチェック
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"

  # npmの依存関係をチェック
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"

Tip 2: Makefileで開発体験を向上させる

docker-compose updocker exec ... のようなコマンドを毎回入力するのは面倒です。Makefileを使って、よく使う操作をシンプルなコマンドにラップしましょう。

blog-themeリポジトリのルートに以下のようなMakefileを置きます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
.PHONY: help setup server build clean

help:
	@echo "Usage: make [target]"
	@echo "targets:"
	@echo "  setup     Install dependencies"
	@echo "  server    Start development server"
	@echo "  build     Build static files for production"
	@echo "  clean     Remove generated files and node_modules"

setup:
	docker-compose build

server:
	docker-compose up

build:
	docker-compose run --rm hugo hugo --minify

clean:
	rm -rf public resources node_modules

これにより、make serverで開発サーバーを起動、make buildで本番用のビルドを実行、といった直感的な操作が可能になり、開発体験が大きく向上します。

Tip 3: プルリクエストでプレビュー環境を自動構築

blog-contentsリポジトリに新しい記事のプルリクエストが作成された際、変更内容を実際のサイトで確認できるプレビュー環境が自動で立ち上がると、レビューが格段に捗ります。 Cloudflare PagesやVercel、Netlifyといったホスティングサービスは、このプレビューデプロイ機能に優れています。GitHub Actionsのワークフローを少し変更し、PRイベントをトリガーにしてこれらのサービスにデプロイするジョブを追加するだけで、魔法のようなプレビュー体験が実現できます。

まとめ

今回は、不安定化したブログ自動化システムを、根本原因から見直して再構築した道のりをご紹介しました。

改めて、今回の取り組みの要点を振り返ります。

  • 問題: システムの不安定性は、「環境の不一致」と「関心の分離の欠如」という2つの根深い問題に起因していた。
  • 解決策:
    1. Dockerによるビルド環境のコンテナ化で、完全な「再現性」を確保した。
    2. マルチレポ戦略によるリポジトリ分割で、「関心の分離」を徹底し、メンテナンス性を向上させた。

この取り組みから得られた最大の教訓は、自動化システム(CI/CDパイプライン)もまた、一つの重要なアプリケーションであるということです。「とりあえず動けばいい」という場当たり的な実装は、必ず将来の技術的負債となって自分に返ってきます。アプリケーションコードと同様に、クリーンな設計、リファクタリング、そして継続的な改善が不可欠なのです。

もし今、あなたの自動化システムが悲鳴を上げているなら、それはアーキテクチャを見直す絶好の機会かもしれません。この記事で紹介した「再現性の確保」と「関心の分離」という2つの原則が、あなたのシステムの安定化に向けた確かな道しるべとなることを願っています。

Happy Automating