【第4回】GitHub Actions × Python 自動化入門 — ワークフロー設計のベストプラクティスとよくあるハマりどころ

プログラミング

この記事でわかること

  • GitHub Actionsのscheduleトリガーのタイムゾーン設定方法
  • 定期実行が「時間通り」に動かない理由と対策
  • 60日間アクティビティなしで自動停止される仕様と回避方法
  • Pythonスクリプト実行時のベストプラクティス
  • 実際のワークフロー設計例と注意点

前回までのおさらい

前回(第3回)は、GitHub ActionsからAWSリソースにアクセスする際のOIDC認証まわりを紹介しました。長期的なアクセスキーを使わずにAssumeRoleできるようにする話で、個人的に「これ知ってから設計がずいぶん楽になったな」と感じた内容です。

今回は、シリーズを締めくくるテーマとしてワークフロー設計全体のベストプラクティスと、自分が実際にハマったポイントをまとめてみます。特に定期実行(schedule)まわりは罠が多いので、そこを中心に書きます。

scheduleトリガーの基本と注意点

GitHub Actionsで定期実行するには on.schedule にcron式を書くだけです。

on:
  schedule:
    - cron: '0 0 * * *'  # 毎日 00:00 UTC
  workflow_dispatch:      # 手動実行も残しておく

ただし、ここには大きな落とし穴があります。GitHub ActionsのscheduleはUTC基準で動くので、日本時間(JST)で設定する場合は注意が必要です。JSTはUTC+9なので、「毎朝9時に実行したい」なら 0 0 * * * になります。毎回ここで計算ミスをするので、個人的には最初にコメントでJST換算を書くようにしています。

on:
  schedule:
    # 毎日 09:00 JST = 00:00 UTC
    - cron: '0 0 * * *'
  workflow_dispatch:

あと workflow_dispatch は絶対に一緒に書いておくことをおすすめします。デバッグのときに手動でポチれないと本当につらい。

scheduleは「時間通り」には動かない

これ、知らずにハマる人が多いと思うので強調しておきます。

GitHub Actionsのscheduleイベントは、GitHubのActions実行負荷が高い時間帯には遅延します。実際のところ、個人の経験ではワークフローの開始が20〜30分ほど遅れることはざらにあります。

自分の経験でも、「9時に通知が来るはずなのに10時近くになっても来ない」みたいなことが普通にありました。「バグかな?」と思ってログを漁ったら、単純に遅れていただけ、というやつ。

GitHubのドキュメントにも「負荷が高い場合、scheduleイベントは遅延しうる」「毎時ちょうど(start of every hour)は高負荷になりやすいので、遅延を減らすために時間をずらすとよい」と注記されています。混雑を避けるために「毎時0分」より「毎時17分」みたいな中途半端な時間に設定するほうが、比較的安定します。

on:
  schedule:
    # 毎日 09:17 JST。混雑を避けるためわざと中途半端な分数にしている
    - cron: '17 0 * * *'
  workflow_dispatch:

厳密なスケジュール実行が必要な場合は、正直GitHub ActionsよりもAWS EventBridgeやCloud Schedulerを使うほうが確実です。自分のユースケース(日次のデータ集計くらい)では多少ズレても問題ないので、Actionsに任せています。

60日間アクティビティなしで自動停止される

これも地味に厄介な仕様です。

パブリックリポジトリでは、60日間リポジトリにコミットなどのアクティビティがない場合、スケジュールワークフローが自動的に無効化されます。「This scheduled workflow is disabled because there hasn’t been activity in this repository for at least 60 days」というメッセージが表示されることがあります。

「あれ、なんか先月から動いてないな?」と気づいて確認したら停止されていた、というのは自分もやらかしました。特に個人の自動化リポジトリは更新頻度が低くなりがちなので要注意です。

この問題への対策として、keepalive-workflow というGitHub Actionが公開されています。定期的にダミーコミットを作ることで、60日ルールをかいくぐれます。

# .github/workflows/keepalive.yml
name: Keepalive

on:
  schedule:
    - cron: '0 0 1 * *'  # 毎月1日に実行

jobs:
  keepalive:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: gautamkrishnar/keepalive-workflow@v2

余談ですが、この問題はプライベートリポジトリには適用されないっぽいです。公式ドキュメント系の説明だと「パブリックリポジトリで」という文脈で語られるので、プライベートなら気にしなくていいかもしれません。

Pythonスクリプトを動かす実装例

ここからは実際のワークフロー構成例です。「定期的にAPIからデータを取得してDynamoDBに保存する」みたいなスクリプトを動かすことを想定しています。

# .github/workflows/daily_fetch.yml
name: Daily Data Fetch

on:
  schedule:
    - cron: '17 0 * * *'
  workflow_dispatch:

jobs:
  fetch:
    runs-on: ubuntu-latest
    timeout-minutes: 15  # ここ注意:省略するとデフォルト360分
    permissions:
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pip'

      - name: Install dependencies
        run: pip install -r requirements.txt

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1

      - name: Run script
        env:
          SOME_API_KEY: ${{ secrets.SOME_API_KEY }}
        run: python scripts/fetch_data.py

いくつかポイントを補足します。

timeout-minutesは必ず設定する

書き忘れがちですが、これを省略するとデフォルトで6時間待ちます。スクリプトがどこかで詰まったまま6時間走り続けるのはActionsの無料枠をジワジワ消費するので、必ず明示的に設定しましょう。15〜30分くらいが自分の感覚的な目安です。

cache: ‘pip’ で地味に速くなる

actions/setup-pythoncache: 'pip' を指定するだけで、依存パッケージのインストール時間が短縮されます。特に boto3 などパッケージ数が多くなりがちな場合は効果が出やすいです。毎回インストールするのと比べると、体感で1〜2分は変わります。

Secretsはenvで明示的に渡す

GitHubのSecretsをワークフロー内で使う場合、env ブロックで明示的に環境変数として渡すのがベストプラクティスです。ログには自動的にマスクされて出力されます。

ただし、環境変数名はわかりやすくつけましょう。SECRET1 みたいな名前にすると3ヶ月後に自分が泣きます。これは実体験です。

concurrencyでジョブの重複実行を防ぐ

スケジュール実行と workflow_dispatch を両方有効にしていると、タイミングが重なって同じジョブが二重起動することがあります。これを防ぐのが concurrency 設定です。

name: Daily Data Fetch

on:
  schedule:
    - cron: '17 0 * * *'
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: false  # 実行中のジョブを優先して新規をスキップ

cancel-in-progress: true にすると「新しいほうを優先して古いものをキャンセル」、false にすると「実行中のものを優先して新しいほうを待機させる」挙動になります。データ処理系のバッチは途中でキャンセルされると中途半端な状態になりがちなので、自分は基本 false にしています。デプロイ系なら true のほうがいい場面もあるので、用途に合わせて。

ワークフロー設計時のチェックリスト

シリーズを通じて自分がハマったポイントをまとめておきます。

  • タイムゾーン:cronはUTC基準。JSTは+9時間で逆算する
  • 遅延への対策:scheduleは数分〜数十分ずれることがある。毎時0分は避けて、時間をずらす
  • 60日停止対策:パブリックリポジトリは非アクティブで自動停止される。keepalive-workflowを使うか、プライベートリポジトリにする
  • timeout-minutes:デフォルト360分。必ず明示設定する
  • 重複実行concurrency を設定して防ぐ
  • Secrets名:あとで読んでわかる名前をつける。絶対に

「そんなの当たり前では?」と思う項目もあるかもしれないですが、これ全部リアルにやらかしたか、ひやっとした経験があります。特に60日停止は完全に盲点でした。

※この記事にはプロモーションが含まれます

ちなみに、お名前.com レンタルサーバー(WordPressに特化した高速レンタルサーバー。月額990円〜、独自ドメイン実質0円)も気になっています。お名前.com レンタルサーバー

📚 シリーズ「GitHub Actions × Python 自動化入門」(第4回 / 全4回)

← 前回の記事: 前回の記事はこちら

🎉 このシリーズは今回で完結です!

参考になったらクリックしてもらえると嬉しいです!

Blogmura ProgrammingProgramming Ranking
タイトルとURLをコピーしました