GitHub ActionsのWorkflowを改善した

GitHub Actions

このブログは、Hugo+GitHub Pagesで公開しているが、CI/CDにGitHub Actionsを活用している。 以下の過去記事で構成を記載している。

Workflowは、わりと雑に作ってありpush毎にCIが動きmainブランチへのマージでCI/CDが動くようになっていた。 これだと毎回CIが動くため非効率。CIは環境構築含め約2分30秒くらいかかっており、デプロイまで約5分かかっていた。

以下構成に変更したく、試行錯誤してみた。

Workflow構成

最終的に以下構成にした。

結果的に、ワークフローが以下のように改善した

改善前のワークフロー(約5分かかってる)
改善後のワークフロー(約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 の名前に対して評価されます。

参照: GitHub Docs - ワークフローのトリガー

pushイベントの場合は、pushするブランチのフィルタとなる。僕はここを勘違いしていてはまってしまった。

push イベントの branches フィルターでは、プッシュが発生したときではなく、branches フィルターと同じブランチに対してプッシュが発生したときのみ、ワークフローを実行できます。

参照: GitHub Docs - ワークフローのトリガー:フィルタを使用する

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のワークフロー分離

以下方法を試した。

GitHubで完結する方がよいので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チートシート - zenn

もうひとつ大切なポイントとして、GPGの鍵存在チェックを行うようにしている点。「Check if GPG key exists」stepがチェック処理。 GitHub Actionsのワークフローを複数回実行した場合、追加した鍵が存在したままになる。Re-runしても問題ないようにチェックを追加した。

その他対応したこと

ワークフローを実装するためにいろんなブログを参考にした。 :set-outputコマンドを使うサンプルがそこそこあり活用していたが、実はdeprecated機能。実行後のログでもワーニングでdeprecatedが表示された。

set-outputのワーニングエラー

この機会なので$GITHUB_ENVを利用した書き方に変更。こちらの方が個人的にわかりやすいと思う。

echo "key_name=value" >> $GITHUB_ENV

参考にしたサイト

まとめ

いろいろ試行錯誤してわりと作業に時間かかってしまったが期待する構成にできた。ブログの公開がスムーズになって嬉しい。 おかげで、雰囲気で書いていたGitHub Actionsの基本的なところを理解できた。

See Also