HugoブログをGithubActionを使ってS3サーバに差分アップする方法
はじめに
Hugoを使ってこのブログを書いているのですが、ブログのコンテンツが増えてきてGithub Actionで実行時間超過によりFailedするようになってきました。 今まではS3サーバのコンテンツを全消しして、Github Actionからビルドしたコンテンツをアップロードするスクリプトを書いてました。 Github Action上だと、Workflowというんですかね。
過去の記事で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や鍵、アップロード先のバケット名を保存しています。
今回の修正方針
差分ビルドを実現するためには、前回のビルドからの変更ファイルを検出し、それらのファイルだけをビルドしてアップロードする必要があります。
- 前回のコミットとの差分ファイルを取得します。
- 差分ファイルを元にHugoで部分的にビルドを行います。
- ビルドされた差分ファイルを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 |
こんな風にまとめてくれるのも最高じゃないか。