HugoブログをGithubActionを使ってS3サーバに差分アップする方法

2023-03-20-01-21-05.webp
目次

はじめに

Hugoを使ってこのブログを書いているのですが、ブログのコンテンツが増えてきてGithub Actionで実行時間超過によりFailedするようになってきました。 今まではS3サーバのコンテンツを全消しして、Github Actionからビルドしたコンテンツをアップロードするスクリプトを書いてました。 Github Action上だと、Workflowというんですかね。

hugoを使ってssgサイトを作ろう4

過去の記事でGithub Actionを使ってアップロードをする方法を書いてます。 今回はこの記事の手法を改善していきます。

Github Actionについて

改めて、Github Actionについておさらいしておきます。

GitHub Actionsは、GitHubリポジトリ内でCI(Continuous Integration)/ CD(Continuous Deployment)タスクやその他の自動化タスクを実行するための機能です。 これにより、コードのビルド、テスト、デプロイなどの一連のアクションを自動化できます。

GitHub Actionsの主な概念は以下の通りです。

  • ワークフロー(Workflows): ワークフローは、リポジトリ内の.github/workflowsディレクトリに配置されるYAMLファイルで定義されます。ワークフローは、一連のアクションを含むプロセスで、イベント(プッシュ、プルリクエスト、スケジュールなど)に基づいて自動的にトリガーされます。

  • ジョブ(Jobs): ワークフローは、複数のジョブで構成されます。ジョブは、それぞれ独立した環境で実行され、並行して実行されることができます。ジョブの実行順序は、needsキーワードを使用して制御できます。

  • ステップ(Steps): 各ジョブは、順番に実行される複数のステップで構成されます。ステップは、独自のアクションを実行するか、他の人が作成した既存のアクションを使用できます。ステップは、シェルコマンドやGitHub Actions Marketplaceから取得したアクションを使用して実行できます。

  • アクション(Actions): アクションは、ステップで使用される再利用可能なコード片です。アクションは、他の開発者が公開したものをGitHub Actions Marketplaceから取得できるほか、独自に作成できます。

  • イベント(Events): ワークフローは、特定のイベント(プッシュ、プルリクエスト、リリースなど)によってトリガーされます。onキーワードを使用して、ワークフローがどのイベントに対応するかを指定できます。

  • ランナー(Runners): ランナーは、ジョブを実行するための環境です。GitHubは、Linux、macOS、およびWindowsのホストランナーを提供しています。また、独自のセルフホストランナーを設定できます。

Github Actionを使うことで、このような仕組みを組み合わせ、GithubにPushした瞬間、サーバ上でビルドを走らせてサイトの更新など連携しソフトウェアの品質を高めることができます。

前述の通り、このブログはHugoで作られており、ビルド結果をS3バケットにアップしています。

改めての困りごと

ビルド記事が増えて、処理時間内に終えることができず、このように処理途中でキャンセルされるようになっていました。

原因は、S3バケットを全クリアしてビルド結果を全てアップしているからだと考えます。 そもそもアップする量が多すぎるんですよね。

Github Workflow

S3 Uploadスクリプトはこのように準備してました。 このファイルをGithubレポジトリ下記フォルダに格納してました。

.github/workflows/hugo_deploy.yml

このファイルを修正していきます。

name: S3 Upload

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 5

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: "0.101.0"
          extended: true

      - name: Build Hugo
        run: |
          hugo --environment production
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-3

      - name: Upload file to S3
        env:
          S3_UPLOAD_BUCKET: ${{ secrets.S3_UPLOAD_BUCKET }}
        run: |
          aws s3 sync --exact-timestamps --delete public s3://$S3_UPLOAD_BUCKET/

AWS_ACCESS_KEY_IDなどは、GithubのレポジトリSettingのSecurityから設定しています。 Secrets and variablesでこのようなイメージです。

S3にアクセスするためのIDや鍵、アップロード先のバケット名を保存しています。

今回の修正方針

差分ビルドを実現するためには、前回のビルドからの変更ファイルを検出し、それらのファイルだけをビルドしてアップロードする必要があります。

  1. 前回のコミットとの差分ファイルを取得します。
  2. 差分ファイルを元にHugoで部分的にビルドを行います。
  3. ビルドされた差分ファイルをS3にアップロードします。

ただし、Hugoでは部分的なビルドが直接サポートされていないため、記事の更新日時をチェックしその情報に基づいてビルドするスクリプトを作成して対応します。

Github Action Workflowの改善案

こんな風に書いてみます。

name: S3 Upload

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 5

    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: "0.101.0"
          extended: true

      - name: Build Hugo
        run: |
          hugo --environment production
      - name: Build updated content
        run: |
          git diff --name-only HEAD HEAD~1 > changed-files.txt
          python3 build-updated-content.py changed-files.txt
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-3

      - name: Upload updated content to S3
        env:
          S3_UPLOAD_BUCKET: ${{ secrets.S3_UPLOAD_BUCKET }}
        run: |
          cat updated-content.txt | while read line; do
            if [ -e "$line" ]; then
              echo "Uploading $line"
              aws s3 cp "$line" "s3://$S3_UPLOAD_BUCKET/$(basename $line)"
            else
              echo "Skipping non-existent file: $line"
            fi
          done

このスクリプトの肝となるのが、Pythonで書いたBuild-updated-content.pyです。 このスクリプトではGithub上で変更されたファイルリストを受け取っています。 そして再ビルドを行い、アップロードするファイルのリスト作成します。

updated-content.txtファイルには、アップロードするビルド済みファイルのパスが含まれています。

          cat updated-content.txt | while read line; do
            if [ -e "$line" ]; then
              echo "Uploading $line"
              aws s3 cp "$line" "s3://$S3_UPLOAD_BUCKET/$(basename $line)"
            else
              echo "Skipping non-existent file: $line"
            fi
          done

この部分で存在しないファイルが残っていた場合とかはSKIPするようにしてます。 こんな感じでどうですかね。

import sys
import os
import subprocess
from pathlib import Path

def build_updated_content(changed_files_txt):
    with open(changed_files_txt, 'r') as f:
        changed_files = f.read().splitlines()

    # Filter only content files
    content_files = [f for f in changed_files if f.startswith('content/')]

    # List of updated built files
    updated_built_files = []

    for content_file in content_files:
        # Convert content file path to public file path
        public_file = content_file.replace('content/', 'public/').replace('.md', '/index.html')

        # Add public file to updated built files list
        updated_built_files.append(public_file)

    # Save updated built files list to a file
    with open('updated-content.txt', 'w') as f:
        for item in updated_built_files:
            f.write("%s\n" % item)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python3 build-updated-content.py <changed-files.txt>")
        sys.exit(1)

    changed_files_txt = sys.argv[1]
    build_updated_content(changed_files_txt)

記事のパスを変換して、対応するビルド済みファイルのパスに変換しています。 ただ、この方法では、記事の依存関係に変更があった場合には正しくビルドされない場合があるので注意が必要です。 まぁこのブログの運用上あまりない話なので一旦は良いです。

そもそも、Hugoが部分的なビルドを直接サポートしていないのが良くないんですよね。(対応してないよね…?) なので、今回の方針では、全ビルドを最初にやってから、変更されたコンテンツファイルに対応するビルド済みファイルを抽出してアップロードするような方法を取っています。

保存場所

GitHub Actionsは、リポジトリのルートディレクトリからの相対パスを使用してファイルを参照しているので、このようなフォルダ構成にしました。

repository_root/
  ├── .github/
  │   └── workflows/
  │       └── your_workflow.yml
  ├── content/
  ├── public/
  ├── static/
  ├── themes/
  ├── config.toml
  └── build-updated-content.py

Pythonスクリプトを実行するのは、Workflowの設定ファイルではこのように書いています。

- name: Build updated content
  run: |
    git diff --name-only HEAD HEAD~1 > changed-files.txt
    python3 build-updated-content.py changed-files.txt

まとめ

無事やりたいことが実現できました。 ブログの更新自体も高速になりました。

元々、5分ちょっとかかっていた処理が、2分ちょっとで終わるようになっています。

ChatGPTを使って、引用元とか参考情報と集めてもらって、Markdown形式とかにまとめてもらったんだけど、ホントめっちゃ優秀ですね。 あと、文書の叩き台作って構成して、一般知識を補完してって使い方が便利過ぎる。もうこの子がいないのが考えられない。

記事を作成する上で参考にした情報

No. ソース名称 URL
1 GitHub Actions 公式ドキュメント https://docs.github.com/en/actions
2 ワークフロー構文 https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
3 Hugo 公式ドキュメント https://gohugo.io/documentation/
4 コンテンツ管理 https://gohugo.io/content-management/
5 テンプレート https://gohugo.io/templates/
6 actions/checkout https://github.com/actions/checkout
7 peaceiris/actions-hugo https://github.com/peaceiris/actions-hugo
8 aws-actions/configure-aws-credentials https://github.com/aws-actions/configure-aws-credentials
9 AWS CLIのS3コマンドリファレンス https://awscli.amazonaws.com/v2/documentation/api/latest/index.html
10 S3コマンド https://awscli.amazonaws.com/v2/documentation/api/latest/reference/s3/index.html

こんな風にまとめてくれるのも最高じゃないか。

Related Post

> HugoブログをGithubActionを使ってS3サーバに差分アップする方法
Hugoで404ページを作る
> HugoブログをGithubActionを使ってS3サーバに差分アップする方法
ブログでの使用画像をWebpフォーマットに変換してサイト軽量化する
> HugoブログをGithubActionを使ってS3サーバに差分アップする方法
Hugoを使ってSSGサイトを作ろう4
> HugoブログをGithubActionを使ってS3サーバに差分アップする方法
Hugoを使ってSSGサイトを作ろう3
> HugoブログをGithubActionを使ってS3サーバに差分アップする方法
Hugoを使ってSSGサイトを作ろう2
> HugoブログをGithubActionを使ってS3サーバに差分アップする方法
Hugoを使ってSSGサイトを作ろう

おすすめの商品

>