GitHub ActionsのWorkflowを改善した
このブログは、Hugo+GitHub Pagesで公開しているが、CI/CDにGitHub Actionsを活用している。 以下の過去記事で構成を記載している。
Workflowは、わりと雑に作ってありpush毎にCIが動きmainブランチへのマージでCI/CDが動くようになっていた。 これだと毎回CIが動くため非効率。CIは環境構築含め約2分30秒くらいかかっており、デプロイまで約5分かかっていた。
以下構成に変更したく、試行錯誤してみた。
- 開発ブランチのPull Request作成時に、Hugoビルド+CIを行い問題なければ成果物を保持
- mainブランチへのマージをきっかけに、上の成果物をGitHub Pagesに反映
Workflow構成
最終的に以下構成にした。
- CIワークフローとDeployワークフローの2つに分割
- Pushイベントではなく、Pull Requestイベントでワークフローが動くように変更
- CIワークフロー内でhugoのビルドを行い、publicフォルダをS3にアップロード
- publicフォルダはtar.gzに圧縮
- アップロードファイルはGPG署名
- GitHub Pagesへのデプロイは、S3から資源をダウンロード
- ダウンロードファイルはGPG署名検証
- 署名検証で問題なければ、GitHub Pagesに反映
結果的に、ワークフローが以下のように改善した
- ブログ反映(mainブランチへのマージ時)
- before: 約5分
- after: 約20秒
試行錯誤1: イベントの変更
pushイベントよりPull Requestベースで動いてくれた方がコントロールしやすいため変更。
これにより、$github.ref
が参照するブランチが変わる。Pull Requestイベントの場合作業ブランチになる。
公式ドキュメントには以下のように記載されている
pull_request によってトリガーされるワークフローの場合、これは pull request のマージ ブランチです。
参照: GitHub Docs - Accessing contextual information about workflow runs
またイベントのブランチフィルターは、 $github.ref
に対するフィルターになる
branches で定義されているパターンは、Git ref の名前に対して評価されます。
pushイベントの場合は、pushするブランチのフィルタとなる。僕はここを勘違いしていてはまってしまった。
push イベントの branches フィルターでは、プッシュが発生したときではなく、branches フィルターと同じブランチに対してプッシュが発生したときのみ、ワークフローを実行できます。
GitHub Flowで作業しているためmainブランチに対してPull Requestを作成する。CIワークフローは以下の用にした。
on:
pull_request:
types: [ opened, synchronize, reopened ]
branches:
- main
jobs:
ci:
if: ${{ github.ref != 'refs/heads/main' }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
...
Deployワークフローは、Pull Requestによるマージタイミング(実態は、mainブランチへのマージ)でデプロイしたい。しかしPull RequestのCloseでは動いてほしくない。 以下のように、マージされたかのチェックをいれた。
on:
pull_request:
types: [ closed ]
branches:
- main
jobs:
deploy:
if: github.ref == 'refs/heads/main' && github.event.pull_request.merged == true
...
試行錯誤2: CI/CDのワークフロー分離
以下方法を試した。
- Artifactsを利用してデプロイする
- どこかにデプロイ資源を置いてデプロイする
GitHubで完結する方がよいのでArtifactsを試した。しかし以下の点で断念。
- Artifactsはワークフローに紐つく
- あるワークフローからデプロイすべき別のワークフローのArtifactsを特定するのが困難
Artifactsは、あるワークフローでの成果物を参照可能(DL可能)にするような使い方、もしくは同一ワークフローで成果物を使いまわす際に使える。 今回のビルド成果物は、別のワークフローで使いたいためあわなかった。
そこで、「どこかにデプロイ資源を置いてデプロイする」方法に変更。S3にアップロード/ダウンロードすれば、異なるワークフローでも処理可能。 イベントもPull Requestベースに変えたため、Pull Request IDベースでデプロイ資源を管理できる。
S3へのアップロードは、以下のように実施している。
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Upload Deploy files to S3
env:
S3_DEPLOY_BUCKET: ${{ secrets.AWS_DEPLOY_S3_BUCKET }}
run: |
if aws s3 cp ./${{ env.deploy-file }} s3://$S3_DEPLOY_BUCKET/path/to/${{ github.event.number }}/ --quiet; then
echo 'Upload Success to S3'
echo 'PR Number = ${{ github.event.number }}'
else
echo 'Failed Upload to S3'
exit 1
fi
試行錯誤3: 署名・署名検証
S3(外部ストレージ)を利用するためデプロイファイルを外から指定できてしまう恐れがある。
第三者からファイルを差し込まれたり改変される危険性があるため、デプロイファイルはGitHub Actions上で署名・署名検証を行う。 これにより、署名されていないファイルや不正なファイルは適用されない。
署名・署名検証にはGPGを活用した。以下のように署名・署名検証を行う。
- name: Set GPG_TTY
run: export GPG_TTY=$(tty)
- name: Check if GPG key exists
id: check_secret_key
env:
KEY_ID: ${{ secrets.GPG_KEY_ID }}
run: |
if gpg --list-secret-keys --keyid-format LONG | grep -q "${{ env.KEY_ID }}"; then
echo "key_exists=true" >> $GITHUB_ENV
else
echo "key_exists=false" >> $GITHUB_ENV
fi
- name: Import GPG key
if: env.key_exists == 'false'
env:
PRIVATE_KEY_BASE64: ${{ secrets.GPG_PRIVATE_KEY_BASE64 }}
PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
KEY_ID: ${{ secrets.GPG_KEY_ID }}
run: |
echo -n "${{ env.PRIVATE_KEY_BASE64 }}" | base64 --decode | gpg --batch --yes --import
- name: Trust GPG Key
if: env.key_exists == 'true'
env:
PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
KEY_ID: ${{ secrets.GPG_KEY_ID }}
run: |
echo -e "5\ny\n" | gpg --batch --yes --pinentry-mode loopback --passphrase "${{ env.PASSPHRASE }}" --command-fd 0 --edit-key ${{ env.KEY_ID }} trust quit
- name: Sign artifact with GPG
env:
PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
if gpg --batch --yes --pinentry-mode loopback --passphrase "${{ env.PASSPHRASE }}" --armor --output ${{ env.deploy-file }}.sig --detach-sig ${{ env.deploy-file }}; then
echo 'Sign GPG'
else
echo 'Failed Sign GPG'
exit 1
fi
署名するために秘密鍵が必要になるが改行を含むデータになる。GitHubのSecretは、改行を含むデータは登録できない(改行までのデータになってしまう)。 そのためBase64エンコード・デコードして改行を扱えるようにしている。
また、GPGコマンドはバッチモードかつTTY指定してコマンドで鍵やパスフレーズ指定ができるようにしている。
echo -e "5\ny\n"
部分はgpg trustコマンドで「5 = 究極的に信用する」を設定するために渡しているのだが、以下記事の画面イメージを見てもらうと想像しやすい。
もうひとつ大切なポイントとして、GPGの鍵存在チェックを行うようにしている点。「Check if GPG key exists」stepがチェック処理。 GitHub Actionsのワークフローを複数回実行した場合、追加した鍵が存在したままになる。Re-runしても問題ないようにチェックを追加した。
その他対応したこと
ワークフローを実装するためにいろんなブログを参考にした。
:set-output
コマンドを使うサンプルがそこそこあり活用していたが、実はdeprecated機能。実行後のログでもワーニングでdeprecatedが表示された。
この機会なので$GITHUB_ENV
を利用した書き方に変更。こちらの方が個人的にわかりやすいと思う。
echo "key_name=value" >> $GITHUB_ENV
参考にしたサイト
- GitHub Actions でプルリクのマージでワークフローを実行する
- PR に紐付けたいワークフローは push ではなく pull_request イベントを使おう
- GitHub Actions のベストプラクティス
- GitHub ActionsにおけるStep/Job/Workflow設計論
- Amazon S3のアクセスに必要な最低限のIAMポリシーの設定
- GitHub Actions で OIDC を使用して AWS 認証を行う
- GitHub Actionsで連続pushした時に止めるアレ
- gpg-agent forwarding: inappropriate ioctl for device
- GitHub Actions: Deprecating save-state and set-output commands
- GitHub ActionsワークフローでAmazon S3のオブジェクトにアクセスする
まとめ
いろいろ試行錯誤してわりと作業に時間かかってしまったが期待する構成にできた。ブログの公開がスムーズになって嬉しい。 おかげで、雰囲気で書いていたGitHub Actionsの基本的なところを理解できた。