<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>GitHub Actions on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/github-actions/</link>
    <description>Recent content in GitHub Actions on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Sat, 07 Mar 2026 13:00:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/github-actions/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>GitHub Actionsでモノレポを安全に自動リリースする設計: 変更検知・段階配布・失敗復旧</title>
      <link>https://www.ai2core.com/posts/2026-03-07-github-actions-monorepo-release-automation-guide/</link>
      <pubDate>Sat, 07 Mar 2026 13:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-07-github-actions-monorepo-release-automation-guide/</guid>
      <description>モノレポ運用でGitHub Actionsを使い、変更検知から段階リリース、ロールバック、監査まで実装する具体手順を解説。</description>
      <content:encoded><![CDATA[<h1 id="github-actionsでモノレポを安全に自動リリースする設計-変更検知段階配布失敗復旧">GitHub Actionsでモノレポを安全に自動リリースする設計: 変更検知・段階配布・失敗復旧</h1>
<p>モノレポのCI/CDは、単一リポジトリだから楽になる一方で、リリース設計を誤ると一気に難しくなります。</p>
<ul>
<li>1つの変更で全サービスを再デプロイしてしまう</li>
<li>並列ジョブが増えてキュー渋滞する</li>
<li>どのコミットがどのサービスへ反映されたか追跡できない</li>
<li>一部失敗時のロールバックが曖昧</li>
</ul>
<p>本記事では、GitHub Actionsでモノレポを運用しているチーム向けに、実務で耐えるリリース自動化の構成を具体的に説明します。</p>
<h2 id="1-モノレポcicdで先に決める設計原則">1. モノレポCI/CDで先に決める設計原則</h2>
<p>最初に次の原則を明文化します。</p>
<ol>
<li>変更のないサービスはデプロイしない</li>
<li>リリース対象は機械的に決定する</li>
<li>本番反映は段階的（canary/割合配布）</li>
<li>失敗時の復旧手順を自動化する</li>
<li>監査ログ（誰が何をいつ）を残す</li>
</ol>
<p>この5つがないと、運用が属人化し、障害時対応が遅れます。</p>
<h2 id="2-変更検知をワークフローの入口に置く">2. 変更検知をワークフローの入口に置く</h2>
<p>モノレポでは「どのディレクトリが変わったか」を最初に判定し、対象サービスだけを処理します。</p>
<h3 id="21-changed-filesの例">2.1 changed-filesの例</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">detect</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">outputs</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">matrix</span>: <span style="color:#ae81ff">${{ steps.set-matrix.outputs.matrix }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">id</span>: <span style="color:#ae81ff">changed</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">tj-actions/changed-files@v45</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">files_yaml</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            api:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              - services/api/**
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            web:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              - services/web/**
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            worker:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">              - services/worker/**</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">id</span>: <span style="color:#ae81ff">set-matrix</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          python .github/scripts/build_matrix.py &#39;${{ toJson(steps.changed.outputs) }}&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ここでmatrixを作り、後続ジョブを <code>fromJson</code> で動的展開します。</p>
<h2 id="3-reusable-workflowで共通化する">3. reusable workflowで共通化する</h2>
<p>各サービスごとにworkflowを複製すると、保守コストが急上昇します。<code>workflow_call</code> で共通化します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># .github/workflows/release-service.yml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_call</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">inputs</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">service</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">type</span>: <span style="color:#ae81ff">string</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">type</span>: <span style="color:#ae81ff">string</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">release</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">./scripts/release.sh ${{ inputs.service }} ${{ inputs.environment }}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>呼び出し側はサービス名だけ渡せばよくなり、ガバナンスが効きます。</p>
<h2 id="4-同時実行制御concurrencyで事故防止">4. 同時実行制御（concurrency）で事故防止</h2>
<p>同一サービスへの並列デプロイは事故のもとです。<code>concurrency</code> を必ず設定します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">concurrency</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">group</span>: <span style="color:#ae81ff">release-${{ github.ref }}-${{ matrix.service }}</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">cancel-in-progress</span>: <span style="color:#66d9ef">false</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>cancel-in-progress</code> は本番では通常 <code>false</code> が安全です。途中キャンセルで半端状態を作らないためです。</p>
<h2 id="5-環境ごとの保護ルールを使う">5. 環境ごとの保護ルールを使う</h2>
<p>GitHub Environmentsを使えば、本番前レビューやシークレット分離を標準機能で実装できます。</p>
<ul>
<li>staging: 自動デプロイ</li>
<li>production: 承認必須</li>
<li>緊急時のみ管理者がoverride</li>
</ul>
<p>加えて、OIDCでクラウド認証し、長期鍵（固定アクセスキー）を排除します。</p>
<h2 id="6-段階リリースcanaryを組み込む">6. 段階リリース（Canary）を組み込む</h2>
<p>本番へ一気に100%展開は避け、段階配布をワークフローに組み込みます。</p>
<p>例:</p>
<ol>
<li>10%トラフィックへ5分</li>
<li>エラーレート確認</li>
<li>問題なければ50%</li>
<li>最終的に100%</li>
</ol>
<p>疑似コード例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>deploy --service api --traffic <span style="color:#ae81ff">10</span>
</span></span><span style="display:flex;"><span>sleep <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>check_slo api <span style="color:#f92672">||</span> rollback api
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>deploy --service api --traffic <span style="color:#ae81ff">50</span>
</span></span><span style="display:flex;"><span>sleep <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>check_slo api <span style="color:#f92672">||</span> rollback api
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>deploy --service api --traffic <span style="color:#ae81ff">100</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>check_slo</code> はp95レイテンシ・5xx率・ビジネスKPIを含めるのが理想です。</p>
<h2 id="7-失敗復旧を手順ではなく機能にする">7. 失敗復旧を「手順」ではなく「機能」にする</h2>
<p>運用で強いチームは、ロールバックを手作業Runbookではなくワークフロー化しています。</p>
<h3 id="71-rollback-workflow例">7.1 rollback workflow例</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_dispatch</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">inputs</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">service</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">target_sha</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rollback</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">./scripts/rollback.sh ${{ github.event.inputs.service }} ${{ github.event.inputs.target_sha }}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>「事故時はこのボタンを押す」で復旧できる状態を目指してください。</p>
<h2 id="8-リリース証跡の自動生成">8. リリース証跡の自動生成</h2>
<p>監査対応やトラブル解析で必須になるのが証跡です。</p>
<p>最低限、以下をアーティファクト化します。</p>
<ul>
<li>対象サービス一覧</li>
<li>対象コミットSHA</li>
<li>実行者</li>
<li>実行時刻</li>
<li>結果（成功/失敗）</li>
<li>ロールバック有無</li>
</ul>
<p>さらにSlack/Discord通知で、サービス単位の反映結果を即時共有すると現場が安定します。</p>
<h2 id="9-コスト最適化も同時に行う">9. コスト最適化も同時に行う</h2>
<p>モノレポCIは規模が大きくなるほどコストが効いてきます。</p>
<ul>
<li>キャッシュ（依存/ビルド成果物）を徹底</li>
<li>変更のないサービスはskip</li>
<li>夜間バッチと競合しない時間帯に重いjobを寄せる</li>
<li>自己ホストランナーはautoscaling前提で運用</li>
</ul>
<p>「全サービス毎回build」は最初は簡単ですが、半年後に必ず負債化します。</p>
<h2 id="10-導入チェックリスト">10. 導入チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> 変更検知のmatrix生成がある</li>
<li><input disabled="" type="checkbox"> reusable workflowに統一されている</li>
<li><input disabled="" type="checkbox"> concurrency制御がサービス単位で設定済み</li>
<li><input disabled="" type="checkbox"> production環境に承認ルールあり</li>
<li><input disabled="" type="checkbox"> canary + 自動SLO判定がある</li>
<li><input disabled="" type="checkbox"> rollback workflowが存在する</li>
<li><input disabled="" type="checkbox"> リリース証跡を保存している</li>
<li><input disabled="" type="checkbox"> 通知連携（Slack/Discord）が整備されている</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>GitHub Actionsでのモノレポ自動リリースは、単にworkflowを書く作業ではありません。<strong>変更検知・段階配布・復旧自動化・監査</strong> を含む運用設計です。</p>
<ul>
<li>デプロイ対象を最小化する</li>
<li>本番反映は必ず段階化する</li>
<li>失敗時の復旧を自動化する</li>
<li>証跡を残して再現性を確保する</li>
</ul>
<p>この設計を最初に固めると、サービス数が増えても運用は破綻しません。まずは「変更検知 + reusable workflow + rollback workflow」の3点から着手するのが実装コストと効果のバランスが良いです。</p>
<h2 id="付録-失敗しにくい運用設計テンプレート">付録: 失敗しにくい運用設計テンプレート</h2>
<p>最後に、運用へ落とし込むときに有効なテンプレートを示します。実際にはこの3点をチーム標準にするだけで、リリース事故率が下がります。</p>
<ul>
<li><strong>PRテンプレートに「影響サービス」欄を必須化</strong>
<ul>
<li><code>service-a, service-b</code> のように宣言させ、変更検知結果と突合する</li>
</ul>
</li>
<li><strong>デプロイ承認時の確認項目を固定化</strong>
<ul>
<li>監視グラフURL、ロールバック手順URL、オンコール担当を毎回入力</li>
</ul>
</li>
<li><strong>失敗後レビューのフォーマット統一</strong>
<ul>
<li>失敗原因、検知時刻、復旧時刻、再発防止策を必ず残す</li>
</ul>
</li>
</ul>
<p>特にモノレポでは「誰がどこに責任を持つか」が曖昧になりやすいです。workflowの技術実装だけでなく、<strong>承認と振り返りの運用ルール</strong>をセットにして初めて、自動リリースは安定運用に乗ります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>Monorepo</category>
      <category>CI/CD</category>
      <category>Release</category>
      <category>DevOps</category>
    </item>
    <item>
      <title>GitHub Actions OIDCで実現する鍵レス本番デプロイ：漏えい事故を減らす実装プレイブック</title>
      <link>https://www.ai2core.com/posts/2026-03-05-github-actions-oidc-secure-deploy-playbook/</link>
      <pubDate>Thu, 05 Mar 2026 09:08:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-05-github-actions-oidc-secure-deploy-playbook/</guid>
      <description>長期シークレットを廃止し、GitHub Actions OIDCでAWSへ安全にデプロイするための設計・実装・監査手順を具体的に解説。</description>
      <content:encoded><![CDATA[<h1 id="github-actions-oidcで実現する鍵レス本番デプロイ漏えい事故を減らす実装プレイブック">GitHub Actions OIDCで実現する鍵レス本番デプロイ：漏えい事故を減らす実装プレイブック</h1>
<p>CI/CD の事故は、ビルドが失敗することより「漏えいしても気づけない鍵」が残り続けることのほうが深刻です。特に <code>AWS_ACCESS_KEY_ID</code> のような長期シークレットを GitHub Secrets に保存し続ける運用は、便利ですがリスクが高いです。</p>
<p>本記事では、GitHub Actions の <strong>OIDC（OpenID Connect）連携</strong>を使って、長期鍵を使わずに AWS へデプロイする実践手順をまとめます。単なる設定紹介ではなく、<strong>最小権限・ブランチ制限・監査ログ設計</strong>まで含めて、明日から本番投入できる形で説明します。</p>
<h2 id="1-まず何が危険なのか長期シークレット運用の限界">1. まず何が危険なのか：長期シークレット運用の限界</h2>
<p>従来構成では、次のような問題が起きます。</p>
<ul>
<li>Secret が漏れても検知が遅い（CIログ、誤コミット、権限の広いメンバー）</li>
<li>ローテーションが後回しになる</li>
<li>1つの鍵で複数環境へアクセスできてしまう</li>
<li>「誰のどの workflow 実行が何をしたか」が追いにくい</li>
</ul>
<p>OIDC 連携では、GitHub が発行する短命トークンを信頼し、AWS 側で一時認証情報を払い出します。つまり、<strong>保管する鍵そのものを減らす</strong>のが最大の価値です。</p>
<h2 id="2-全体アーキテクチャ">2. 全体アーキテクチャ</h2>
<p>基本フローは以下です。</p>
<ol>
<li>GitHub Actions ジョブが OIDC トークンを取得</li>
<li>AWS IAM の OIDC プロバイダとロール信頼ポリシーで検証</li>
<li>条件に一致したジョブだけ <code>AssumeRoleWithWebIdentity</code></li>
<li>一時クレデンシャルで S3/CloudFront/ECR/ECS へデプロイ</li>
</ol>
<p>ポイントは「GitHub 側の workflow 制御」だけでなく、<strong>AWS 側で repo・branch・workflow を強制する</strong>ことです。</p>
<h2 id="3-aws-側の初期設定oidc-provider--iam-role">3. AWS 側の初期設定（OIDC Provider + IAM Role）</h2>
<h3 id="31-oidc-provider-を作成">3.1 OIDC Provider を作成</h3>
<p>CLI 例（すでに存在する場合はスキップ）:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>aws iam create-open-id-connect-provider <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --url https://token.actions.githubusercontent.com <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --client-id-list sts.amazonaws.com <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="32-信頼ポリシーを厳密化する">3.2 信頼ポリシーを厳密化する</h3>
<p>以下のように <code>sub</code> と <code>aud</code> を必ず絞ります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;Version&#34;</span>: <span style="color:#e6db74">&#34;2012-10-17&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;Statement&#34;</span>: [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Effect&#34;</span>: <span style="color:#e6db74">&#34;Allow&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Principal&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;Federated&#34;</span>: <span style="color:#e6db74">&#34;arn:aws:iam::&lt;ACCOUNT_ID&gt;:oidc-provider/token.actions.githubusercontent.com&#34;</span>
</span></span><span style="display:flex;"><span>      },
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Action&#34;</span>: <span style="color:#e6db74">&#34;sts:AssumeRoleWithWebIdentity&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;Condition&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;StringEquals&#34;</span>: {
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;token.actions.githubusercontent.com:aud&#34;</span>: <span style="color:#e6db74">&#34;sts.amazonaws.com&#34;</span>
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;StringLike&#34;</span>: {
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;token.actions.githubusercontent.com:sub&#34;</span>: <span style="color:#e6db74">&#34;repo:your-org/your-repo:ref:refs/heads/main&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>sub</code> を <code>repo:org/repo:*</code> のように広く取りすぎると、意図しない workflow からも引き受ける可能性があり危険です。</p>
<h3 id="33-デプロイ権限ポリシーを分離する">3.3 デプロイ権限ポリシーを分離する</h3>
<p>「ロール1個に全部盛り」は避けます。</p>
<ul>
<li><code>deploy-web-prod-role</code>: S3同期 + CloudFront invalidation</li>
<li><code>deploy-api-prod-role</code>: ECR push + ECS update</li>
<li><code>read-only-audit-role</code>: CloudWatch Logs / Describe 系のみ</li>
</ul>
<p>環境別（dev/stg/prod）にロールを分離すると、誤デプロイ時の被害半径が大きく減ります。</p>
<h2 id="4-github-actions-workflow-実装">4. GitHub Actions workflow 実装</h2>
<p>最小サンプル:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">deploy-web</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>: [<span style="color:#e6db74">&#34;main&#34;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">permissions</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">id-token</span>: <span style="color:#ae81ff">write</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">contents</span>: <span style="color:#ae81ff">read</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">deploy</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">environment</span>: <span style="color:#ae81ff">production</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Configure AWS credentials via OIDC</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">aws-actions/configure-aws-credentials@v4</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">role-to-assume</span>: <span style="color:#ae81ff">arn:aws:iam::&lt;ACCOUNT_ID&gt;:role/deploy-web-prod-role</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">aws-region</span>: <span style="color:#ae81ff">ap-northeast-1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Build</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          npm ci
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          npm run build</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Upload to S3</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">aws s3 sync ./dist s3://example-prod-web --delete</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Invalidate CloudFront</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: <span style="color:#ae81ff">aws cloudfront create-invalidation --distribution-id E123456 --paths &#34;/*&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>重要なのは <code>permissions.id-token: write</code> を明示する点です。これがないと OIDC トークンを取得できません。</p>
<h2 id="5-事故を防ぐための実務ルール">5. 事故を防ぐための実務ルール</h2>
<h3 id="51-branch-protection-と-environment-protection-を組み合わせる">5.1 branch protection と environment protection を組み合わせる</h3>
<ul>
<li><code>main</code> 直push禁止</li>
<li>必ず PR + 1 approval</li>
<li>production environment には Required reviewers を設定</li>
<li>夜間デプロイを禁止したい場合は手動承認ステップを入れる</li>
</ul>
<h3 id="52-workflow-ファイル改変の監査">5.2 workflow ファイル改変の監査</h3>
<p><code>.github/workflows/*.yml</code> の変更は CODEOWNERS で必ずレビュアー固定にします。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>.github/workflows/*  @platform-team
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="53-self-hosted-runner-の扱い">5.3 self-hosted runner の扱い</h3>
<p>OIDC を導入しても、runner 自体が侵害されると意味が薄れます。</p>
<ul>
<li>runner をプロジェクト共有にしない（専用化）</li>
<li>ジョブ後にワークディレクトリをクリーン</li>
<li>egress 制限をかける</li>
<li>runner グループを環境ごとに分離</li>
</ul>
<h2 id="6-トラブルシューティング">6. トラブルシューティング</h2>
<h3 id="症状1-not-authorized-to-perform-stsassumerolewithwebidentity">症状1: <code>Not authorized to perform sts:AssumeRoleWithWebIdentity</code></h3>
<p>確認ポイント:</p>
<ol>
<li><code>aud</code> が <code>sts.amazonaws.com</code> になっているか</li>
<li><code>sub</code> が実際の実行 ref と一致しているか（タグ実行でハマりやすい）</li>
<li><code>permissions.id-token: write</code> が設定されているか</li>
<li>Role ARN の typo がないか</li>
</ol>
<h3 id="症状2-main-以外で偶発的にデプロイされた">症状2: main 以外で偶発的にデプロイされた</h3>
<ul>
<li>workflow の <code>on.push.branches</code> 見直し</li>
<li>IAM 信頼ポリシー <code>sub</code> を main 固定へ</li>
<li>環境保護ルール（Required reviewers）追加</li>
</ul>
<h3 id="症状3-デプロイは通るが操作が一部失敗">症状3: デプロイは通るが操作が一部失敗</h3>
<p>これは IAM 権限不足の可能性が高いです。CloudTrail の <code>eventName</code> と <code>errorCode</code> を見て、必要最小限のアクションだけ追加します。闇雲に <code>*</code> を付けないこと。</p>
<h2 id="7-段階的な移行計画既存運用からの切替">7. 段階的な移行計画（既存運用からの切替）</h2>
<p>実務では一気に切り替えず、以下の順が安全です。</p>
<ol>
<li>OIDC ロールを作成（既存シークレットは残す）</li>
<li>staging workflow だけ OIDC に切替</li>
<li>1週間監視（失敗率・デプロイ時間・CloudTrail）</li>
<li>production を OIDC 化</li>
<li>最後に長期シークレットを削除</li>
</ol>
<p>削除前に「どの workflow がどの secret を参照しているか」を grep で確認しておくと事故が減ります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git grep -n <span style="color:#e6db74">&#34;AWS_ACCESS_KEY_ID\|AWS_SECRET_ACCESS_KEY&#34;</span> .github/workflows
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="8-監査ログ設計何を見れば安全性が上がるか">8. 監査ログ設計：何を見れば安全性が上がるか</h2>
<p>最低限、次をダッシュボード化すると運用しやすいです。</p>
<ul>
<li><code>AssumeRoleWithWebIdentity</code> 実行回数（日次）</li>
<li>失敗イベント数（権限エラー/条件不一致）</li>
<li>production deploy 実行者（workflow + sha + actor）</li>
<li>1回のデプロイで変更された主要リソース数</li>
</ul>
<p>CloudTrail + CloudWatch Logs Insights での簡易クエリ例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span>fields <span style="color:#f92672">@</span><span style="color:#66d9ef">timestamp</span>, userIdentity.sessionContext.sessionIssuer.userName, eventName, errorCode
</span></span><span style="display:flex;"><span><span style="color:#f92672">|</span> filter eventSource <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;sts.amazonaws.com&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">|</span> filter eventName <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;AssumeRoleWithWebIdentity&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">|</span> sort <span style="color:#f92672">@</span><span style="color:#66d9ef">timestamp</span> <span style="color:#66d9ef">desc</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">|</span> <span style="color:#66d9ef">limit</span> <span style="color:#ae81ff">50</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="まとめ">まとめ</h2>
<p>OIDC は「設定が新しいから導入する」ものではなく、<strong>長期鍵を削減して事故確率を下げるための運用設計</strong>です。導入時にやるべきことはシンプルで、次の3つに集約できます。</p>
<ol>
<li>IAM 信頼ポリシーを repo/branch 単位で厳密化する</li>
<li>workflow 側で id-token 権限と環境保護を設定する</li>
<li>CloudTrail で AssumeRole の監査を継続する</li>
</ol>
<p>ここまで実施すれば、CI/CD のセキュリティは「頑張って守る」状態から、「漏えいしにくい仕組みで守る」状態へ進化します。まずは staging 1本から置き換えるのがおすすめです。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>OIDC</category>
      <category>Security</category>
      <category>AWS</category>
      <category>CI/CD</category>
    </item>
    <item>
      <title>GitHub Actions高速化実践：Matrix戦略・依存キャッシュ・失敗切り分けの設計ガイド</title>
      <link>https://www.ai2core.com/posts/2026-03-04-github-actions-matrix-cache-strategy/</link>
      <pubDate>Wed, 04 Mar 2026 09:05:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-04-github-actions-matrix-cache-strategy/</guid>
      <description>GitHub Actionsの実行時間と失敗率を同時に改善するためのmatrix設計、キャッシュ戦略、並列最適化、トラブルシューティング手順を具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="github-actions高速化実践matrix戦略依存キャッシュ失敗切り分けの設計ガイド">GitHub Actions高速化実践：Matrix戦略・依存キャッシュ・失敗切り分けの設計ガイド</h1>
<p>GitHub Actions は便利ですが、プロジェクトが成長すると「遅い」「不安定」「原因が分かりにくい」という三重苦になりがちです。特に monorepo や複数ランタイム対応（Node/Python/Go など）では、ワークフローの設計次第で CI 時間が 2〜3 倍変わります。</p>
<p>本記事では、<strong>実行時間を短くしながら失敗時の調査コストも下げる</strong>ために、matrix 設計・キャッシュ設計・障害時の確認順序を具体的に整理します。</p>
<h2 id="1-まず何を並列化するかを決める">1. まず「何を並列化するか」を決める</h2>
<p>Actions の高速化は、いきなりキャッシュ最適化から入るより、先にジョブ分解を決める方が効きます。原則は次の通りです。</p>
<ul>
<li>並列化すべき: 独立テスト（OS/バージョン別、サービス別）</li>
<li>直列にすべき: デプロイ、DB マイグレーション、本番反映</li>
<li>依存を分ける: lint/typecheck/test/build を一つに詰め込まない</li>
</ul>
<p>悪い例は、1ジョブに全部詰め込み、失敗時に最初から再実行するパターンです。良い設計では「lint は通るが test だけ失敗」のように切り分けできます。</p>
<h2 id="2-matrix-を作るときの実践ルール">2. matrix を作るときの実践ルール</h2>
<p>matrix は便利ですが、組み合わせ爆発で逆に遅くなることがあります。例えば <code>os x runtime x db</code> をすべて直積にすると、不要なジョブが大量発生します。そこで <code>include/exclude</code> を活用します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">strategy</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">fail-fast</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">matrix</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">os</span>: [<span style="color:#ae81ff">ubuntu-latest, macos-latest]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">node</span>: [<span style="color:#ae81ff">20</span>, <span style="color:#ae81ff">22</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">include</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">os</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">node</span>: <span style="color:#ae81ff">22</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">coverage</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">exclude</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">os</span>: <span style="color:#ae81ff">macos-latest</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">node</span>: <span style="color:#ae81ff">20</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ポイントは次です。</p>
<ol>
<li><strong>基準環境を1つ決める</strong>（例: ubuntu + latest）</li>
<li>カバレッジ計測や重い E2E は基準環境だけで実施</li>
<li>互換性確認は軽量テスト中心にする</li>
</ol>
<p>この設計にすると、品質を落とさずに全体時間を短縮できます。</p>
<h2 id="3-キャッシュは鍵設計が9割">3. キャッシュは「鍵設計」が9割</h2>
<p><code>actions/cache</code> や <code>setup-node</code> / <code>setup-python</code> のキャッシュを入れても、キー設計が甘いとヒット率が低く、逆に復元時間だけ増えます。</p>
<p>Node.js の例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-node@v4</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">node-version</span>: <span style="color:#ae81ff">${{ matrix.node }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache</span>: <span style="color:#e6db74">&#39;npm&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache-dependency-path</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      package-lock.json
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      packages/*/package-lock.json</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Python (pip) の例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-python@v5</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">python-version</span>: <span style="color:#e6db74">&#39;3.12&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache</span>: <span style="color:#e6db74">&#39;pip&#39;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache-dependency-path</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      requirements.txt
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      requirements-dev.txt</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>実務で効くコツ:</p>
<ul>
<li>lockfile をキーに含める（依存変化に追従）</li>
<li>OS・ランタイムバージョンをキーに含める</li>
<li>monorepo は対象サブディレクトリ単位でキー分割</li>
<li>restore-keys を入れすぎない（古いキャッシュ復元で不整合）</li>
</ul>
<h2 id="4-concurrency-で古い実行を止める">4. concurrency で「古い実行を止める」</h2>
<p>PR に連続 push されると、古い CI が残り続けてランナー枯渇を起こします。<code>concurrency</code> を入れて、最新コミットだけ走らせる構成にします。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">concurrency</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">group</span>: <span style="color:#ae81ff">ci-${{ github.workflow }}-${{ github.ref }}</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">cancel-in-progress</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>これだけで無駄実行を大きく減らせます。特にレビュー中に細かい修正を重ねるチームほど効果が高いです。</p>
<h2 id="5-paths-filter-で不要ジョブを起動しない">5. paths-filter で不要ジョブを起動しない</h2>
<p>ドキュメント更新だけなのに全テストが走る、という状態はよくあります。<code>dorny/paths-filter</code> で変更範囲に応じてジョブを分岐します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">dorny/paths-filter@v3</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">id</span>: <span style="color:#ae81ff">changes</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">filters</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      backend:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        - &#39;backend/**&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      frontend:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        - &#39;frontend/**&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 例: backend が変わった時だけ実行</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">if</span>: <span style="color:#ae81ff">steps.changes.outputs.backend == &#39;true&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>これにより、実行時間だけでなくランナーコストも下げられます。</p>
<h2 id="6-失敗時の調査を速くするログ設計">6. 失敗時の調査を速くするログ設計</h2>
<p>CI が遅い組織は、だいたい「失敗調査も遅い」です。改善するには、次の3点を標準化します。</p>
<ul>
<li>失敗したジョブで artifact（ログ、スクリーンショット、coverage）を必ず保存</li>
<li>重要ステップに <code>::group::</code> を付けてログを畳む</li>
<li>flaky テスト検出用に rerun 情報を残す</li>
</ul>
<p>artifact 例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Upload test reports</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">if</span>: <span style="color:#ae81ff">always()</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/upload-artifact@v4</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">test-report-${{ matrix.os }}-${{ matrix.node }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">path</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      reports/
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      coverage/</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>if: always()</code> を忘れると、失敗時ほど証跡が残らないので注意です。</p>
<h2 id="7-self-hosted-runner-を使う場合の注意点">7. self-hosted runner を使う場合の注意点</h2>
<p>高速化目的で self-hosted runner を導入する場合、運用事故が増えやすい領域です。</p>
<ul>
<li>毎回クリーンワークスペース化（残骸で再現不能バグ）</li>
<li>シークレットを runner に永続化しない</li>
<li>パッチ適用・再起動の定期メンテをスケジュール化</li>
<li>runner ラベルを用途別に分離（deploy と test を混在させない）</li>
</ul>
<p>また、デプロイ権限を持つ runner と、PR 由来コードを実行する runner は分離するのが基本です。</p>
<h2 id="8-実際の改善ステップ2週間">8. 実際の改善ステップ（2週間）</h2>
<h3 id="day-1-2-現状計測">Day 1-2: 現状計測</h3>
<ul>
<li>平均実行時間、p95 実行時間、失敗率を取得</li>
<li>一番遅いジョブ上位3つを特定</li>
</ul>
<h3 id="day-3-5-ジョブ分割と-matrix-整理">Day 3-5: ジョブ分割と matrix 整理</h3>
<ul>
<li>lint/typecheck/test/build を分離</li>
<li>matrix の組み合わせを include/exclude で整理</li>
</ul>
<h3 id="day-6-8-キャッシュ最適化">Day 6-8: キャッシュ最適化</h3>
<ul>
<li>lockfile ベースキーへ統一</li>
<li>キャッシュヒット率を可視化</li>
</ul>
<h3 id="day-9-10-無駄実行削減">Day 9-10: 無駄実行削減</h3>
<ul>
<li>concurrency + cancel-in-progress</li>
<li>paths-filter で対象限定</li>
</ul>
<h3 id="day-11-14-運用ルール化">Day 11-14: 運用ルール化</h3>
<ul>
<li>失敗時 artifact を全ジョブ標準化</li>
<li>flaky テスト記録のテンプレート化</li>
</ul>
<p>この手順で進めると、速度改善だけでなく再発防止まで一気に整います。</p>
<h2 id="9-よくある失敗パターン">9. よくある失敗パターン</h2>
<h3 id="パターンa-キャッシュを入れたのに遅い">パターンA: キャッシュを入れたのに遅い</h3>
<p>原因:</p>
<ul>
<li>キーが細かすぎて毎回ミスヒット</li>
<li>圧縮/復元コストが大きいディレクトリを丸ごとキャッシュ</li>
</ul>
<p>対処:</p>
<ul>
<li>依存に限定してキャッシュ</li>
<li>キーに lockfile ハッシュを利用</li>
</ul>
<h3 id="パターンb-matrix-失敗がノイズ化">パターンB: matrix 失敗がノイズ化</h3>
<p>原因:</p>
<ul>
<li><code>fail-fast: true</code> で他環境の情報が取れない</li>
<li>ログ命名が統一されず比較困難</li>
</ul>
<p>対処:</p>
<ul>
<li><code>fail-fast: false</code></li>
<li>artifact 命名規則を統一</li>
</ul>
<h3 id="パターンc-pr-の待ち時間が長い">パターンC: PR の待ち時間が長い</h3>
<p>原因:</p>
<ul>
<li>古いコミットの CI が走り続ける</li>
<li>変更範囲に関係ないジョブが常時起動</li>
</ul>
<p>対処:</p>
<ul>
<li>concurrency で古い実行を停止</li>
<li>paths-filter 導入</li>
</ul>
<h2 id="10-運用チェックリスト">10. 運用チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> ジョブは責務別に分離されている</li>
<li><input disabled="" type="checkbox"> matrix は必要最小限に絞られている</li>
<li><input disabled="" type="checkbox"> 依存キャッシュのキーに lockfile が含まれる</li>
<li><input disabled="" type="checkbox"> concurrency で古い実行をキャンセルしている</li>
<li><input disabled="" type="checkbox"> 変更範囲に応じたジョブ起動制御がある</li>
<li><input disabled="" type="checkbox"> 失敗時 artifact が必ず残る</li>
</ul>
<p>GitHub Actions は「機能を使う」だけでは速くなりません。<strong>実行単位の設計、キャッシュ鍵設計、無駄実行抑制、証跡設計</strong>をセットで行うと、初めて安定した CI 基盤になります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>CI</category>
      <category>DevOps</category>
      <category>Node.js</category>
      <category>Python</category>
    </item>
    <item>
      <title>GitHub Actions再利用ワークフロー運用設計：属人化を防ぎつつ開発速度を上げる実践ガイド</title>
      <link>https://www.ai2core.com/posts/2026-03-02-github-actions-reusable-workflows-governance/</link>
      <pubDate>Mon, 02 Mar 2026 09:08:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-02-github-actions-reusable-workflows-governance/</guid>
      <description>GitHub Actionsの再利用ワークフローを本番運用するための設計・権限管理・移行手順を、具体的なYAML例とチェックリスト付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="github-actions再利用ワークフロー運用設計属人化を防ぎつつ開発速度を上げる実践ガイド">GitHub Actions再利用ワークフロー運用設計：属人化を防ぎつつ開発速度を上げる実践ガイド</h1>
<p>複数リポジトリを運用していると、ほぼ同じ CI 設定を各リポジトリにコピーし続ける状態になりがちです。最初は早く見えますが、半年後には「どこに正解があるのかわからない」状態になります。セキュリティパッチを当てたいだけなのに 20 リポジトリを横断修正し、1つだけ取りこぼして監査で指摘される、というのは珍しくありません。</p>
<p>この問題に効くのが GitHub Actions の <code>workflow_call</code> を使った再利用ワークフローです。ただし、単に共通化するだけでは逆に運用事故が増えることがあります。重要なのは、<strong>共通化の粒度、権限境界、変更リリース方法</strong>を最初に設計することです。</p>
<p>本記事では、実運用で詰まりやすいポイントを中心に、導入から定着までを具体的に解説します。</p>
<h2 id="1-再利用ワークフローの基本方針">1. 再利用ワークフローの基本方針</h2>
<p>まず押さえるべき方針は次の 3 つです。</p>
<ol>
<li><strong>再利用ワークフローは「プラットフォーム製品」として扱う</strong></li>
<li><strong>呼び出し側（各アプリ repo）は薄く保つ</strong></li>
<li><strong>破壊的変更はバージョンを切って段階移行する</strong></li>
</ol>
<p>「共通化＝1ファイルに全部詰め込む」ではありません。lint, test, build, deploy を1個にまとめると、対象外プロジェクトまで影響します。まずは小さく分割し、必要なものだけ組み合わせられる構造にします。</p>
<h2 id="2-推奨ディレクトリ構成">2. 推奨ディレクトリ構成</h2>
<p>共通ワークフローを専用 repo に分離しておくと、監査や変更履歴の管理が容易になります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">9
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>org-ci-workflows/
</span></span><span style="display:flex;"><span>  .github/workflows/
</span></span><span style="display:flex;"><span>    ci-node.yml
</span></span><span style="display:flex;"><span>    ci-python.yml
</span></span><span style="display:flex;"><span>    security-scan.yml
</span></span><span style="display:flex;"><span>    release-tag.yml
</span></span><span style="display:flex;"><span>  docs/
</span></span><span style="display:flex;"><span>    onboarding.md
</span></span><span style="display:flex;"><span>    migration-checklist.md
</span></span></code></pre></td></tr></table>
</div>
</div><p>呼び出し側は以下のように最小化します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">ci</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pull_request</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">push</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">branches</span>: [<span style="color:#ae81ff">main]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">node-ci</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">your-org/org-ci-workflows/.github/workflows/ci-node.yml@v1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">node-version</span>: <span style="color:#e6db74">&#34;22&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">run-e2e</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">secrets</span>: <span style="color:#ae81ff">inherit</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>この形の利点は、アプリチームが理解すべき YAML が減ることです。CI の標準知識が全員に伝播し、レビューも速くなります。</p>
<h2 id="3-実務で効く入力設計inputs">3. 実務で効く入力設計（inputs）</h2>
<p><code>inputs</code> は API 設計だと考えるべきです。曖昧な真偽値が増えると、挙動が読めなくなります。</p>
<h3 id="悪い例">悪い例</h3>
<ul>
<li><code>enable_special_mode: true</code></li>
<li><code>quick: true</code></li>
</ul>
<h3 id="良い例">良い例</h3>
<ul>
<li><code>test-scope: unit|integration|full</code></li>
<li><code>build-target: web|api|worker</code></li>
<li><code>upload-artifact: true|false</code></li>
</ul>
<p>再利用ワークフロー側では型とデフォルト値を明示します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">workflow_call</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">inputs</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">test-scope</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">type</span>: <span style="color:#ae81ff">string</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">default</span>: <span style="color:#e6db74">&#34;unit&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">upload-artifact</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">required</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">type</span>: <span style="color:#ae81ff">boolean</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">default</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>入力名を自然言語に寄せすぎない</strong>のがコツです。将来の運用者が見て意味がぶれない命名に寄せます。</p>
<h2 id="4-セキュリティ設計permissions-を最小化する">4. セキュリティ設計：permissions を最小化する</h2>
<p>再利用ワークフローの導入後に増える事故が「権限過剰」です。<code>permissions: write-all</code> を残したまま運用すると、意図しないトークン利用で供給網リスクが高まります。</p>
<p>推奨はジョブ単位での最小権限化です。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">permissions</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">contents</span>: <span style="color:#ae81ff">read</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pull-requests</span>: <span style="color:#ae81ff">write</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">actions</span>: <span style="color:#ae81ff">read</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>デプロイやタグ作成など書き込みが必要な処理だけ、専用ジョブに分離して <code>contents: write</code> を付与します。CI と CD の境界を分けるだけで、攻撃面積をかなり削減できます。</p>
<h2 id="5-キャッシュ設計でハマらないための実践">5. キャッシュ設計でハマらないための実践</h2>
<p>Actions の高速化でよく使う <code>actions/cache</code> は、キー設計を誤ると逆効果になります。</p>
<ul>
<li>キーが粗い: 依存不整合で不安定化</li>
<li>キーが細かすぎる: キャッシュヒット率低下</li>
</ul>
<p>Node の例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-node@v4</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">with</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">node-version</span>: <span style="color:#ae81ff">${{ inputs.node-version }}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">cache</span>: <span style="color:#ae81ff">npm</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm ci</span>
</span></span><span style="display:flex;"><span>- <span style="color:#f92672">run</span>: <span style="color:#ae81ff">npm test</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>まずは <code>setup-*</code> の組み込みキャッシュを優先し、複雑な手動キャッシュは必要になってから追加する方が安全です。</p>
<h2 id="6-導入手順3週間での現実的移行">6. 導入手順（3週間での現実的移行）</h2>
<h3 id="week-1-現状分析">Week 1: 現状分析</h3>
<ol>
<li>全 repo の workflow を収集</li>
<li>共通ジョブを抽出（lint/test/build/security）</li>
<li>失敗率の高い処理を特定</li>
</ol>
<h3 id="week-2-共通-repo-構築">Week 2: 共通 repo 構築</h3>
<ol>
<li><code>org-ci-workflows</code> を作成</li>
<li>2〜3 種類の再利用ワークフローを作る</li>
<li>テンプレート repo で先行検証</li>
</ol>
<h3 id="week-3-段階移行">Week 3: 段階移行</h3>
<ol>
<li>低リスク repo から順次移行</li>
<li>失敗ログを収集し API（inputs）を微調整</li>
<li><code>v1</code> タグを固定し、全体展開</li>
</ol>
<p>重要なのは、一気に全 repo を置き換えないことです。先行導入で「どの入力が不足しているか」を把握し、後続 repo の移行コストを下げます。</p>
<h2 id="7-変更管理main-直参照を禁止する">7. 変更管理：main 直参照を禁止する</h2>
<p>呼び出し側で <code>@main</code> を使うと、共通 repo の更新が即時反映されます。これは便利ですが、本番では危険です。事故時に「いつ壊れたか」が追跡しづらくなります。</p>
<p>実運用では次を推奨します。</p>
<ul>
<li>呼び出しは <code>@v1</code> などのタグ固定</li>
<li>破壊的変更は <code>v2</code> を新規発行</li>
<li>重大修正のみ <code>v1.x</code> に backport</li>
</ul>
<p>さらに、変更通知テンプレートを用意しておくと移行が速くなります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[CI Workflow Update]
</span></span><span style="display:flex;"><span>- Target: ci-node.yml
</span></span><span style="display:flex;"><span>- Change: npm audit step added
</span></span><span style="display:flex;"><span>- Impact: build time +20-40 sec
</span></span><span style="display:flex;"><span>- Action Required: none (v1.3.0 auto pickup)
</span></span><span style="display:flex;"><span>- Rollback: use v1.2.4
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="8-監視指標導入効果を数字で見る">8. 監視指標（導入効果を数字で見る）</h2>
<p>再利用ワークフローを入れたら、以下を最低でも計測します。</p>
<ul>
<li>CI 平均実行時間</li>
<li>PR あたり失敗回数</li>
<li>ワークフロー設定変更に伴う障害件数</li>
<li>セキュリティ警告検出率</li>
</ul>
<p>導入後 1 か月で、<code>設定差分の削減</code> と <code>障害切り分け時間の短縮</code> が見えてくるはずです。数字が出ない場合は、共通化の粒度が粗すぎる可能性があります。</p>
<h2 id="9-よくある失敗と対策">9. よくある失敗と対策</h2>
<h3 id="失敗1-共通化しすぎて柔軟性が消える">失敗1: 共通化しすぎて柔軟性が消える</h3>
<p>対策: 80%共通 + 20%ローカル上書きの構造にする。例外を許容する。</p>
<h3 id="失敗2-例外入力が増えて-api-が壊れる">失敗2: 例外入力が増えて API が壊れる</h3>
<p>対策: 四半期ごとに <code>inputs</code> を棚卸し。未使用入力を削除し、破壊的変更はメジャーバージョンへ。</p>
<h3 id="失敗3-platform-team-しか触れない">失敗3: Platform Team しか触れない</h3>
<p>対策: docs/onboarding.md を整備し、アプリチームが PR で改善できる運用にする。</p>
<h2 id="10-そのまま使える導入チェックリスト">10. そのまま使える導入チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> 共通 repo を作成し、責任者を明確化した</li>
<li><input disabled="" type="checkbox"> <code>workflow_call</code> の入力仕様を文書化した</li>
<li><input disabled="" type="checkbox"> <code>permissions</code> を最小権限に設定した</li>
<li><input disabled="" type="checkbox"> 呼び出し側でタグ固定（@v1）を徹底した</li>
<li><input disabled="" type="checkbox"> 先行 repo で 1 週間運用検証した</li>
<li><input disabled="" type="checkbox"> ロールバック手順を README に記載した</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>GitHub Actions の再利用ワークフローは、単なる YAML 共通化ではなく、組織の開発基盤を製品として運用する取り組みです。導入の成否は、技術よりも運用設計に左右されます。</p>
<ul>
<li>入力仕様を API として設計する</li>
<li>権限を最小化し事故半径を縮小する</li>
<li>バージョン固定で変更を可観測にする</li>
</ul>
<p>この3点を守るだけで、CI/CD は「壊れやすい魔法」から「改善可能な仕組み」に変わります。まずは 1 つの再利用ワークフローから始め、3 週間で小さく成功を作るのが最短ルートです。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>CI/CD</category>
      <category>DevOps</category>
      <category>Platform Engineering</category>
    </item>
    <item>
      <title>GitHub Actionsセルフホストランナー防衛術：CI/CDの供給網リスクを減らす実装ガイド</title>
      <link>https://www.ai2core.com/posts/2026-02-28-github-actions-selfhosted-runner-security/</link>
      <pubDate>Sat, 28 Feb 2026 13:30:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-28-github-actions-selfhosted-runner-security/</guid>
      <description>セルフホストランナー運用で発生する主要リスクを洗い出し、実装可能な防御策を段階的に紹介。</description>
      <content:encoded><![CDATA[<h1 id="github-actionsセルフホストランナー防衛術cicdの供給網リスクを減らす実装ガイド">GitHub Actionsセルフホストランナー防衛術：CI/CDの供給網リスクを減らす実装ガイド</h1>
<p>セルフホストランナーは高速で柔軟です。特定ツールチェーンや社内ネットワーク接続が必要な環境では、ほぼ必須といえます。一方で、設定を誤ると CI/CD が攻撃経路になります。</p>
<p>近年のインシデントでは、依存パッケージ汚染だけでなく「Actions workflow の権限過多」「fork 由来PRでの秘密情報流出」「ランナー残存データ」が問題化しています。</p>
<p>本記事では、セルフホストランナーを安全に運用するための防衛策を、設計レイヤごとに整理します。</p>
<h2 id="脅威モデルを先に定義する">脅威モデルを先に定義する</h2>
<p>まず守る対象を明確にします。</p>
<ul>
<li>リポジトリのソースコード</li>
<li>Secrets（クラウド鍵、署名鍵、トークン）</li>
<li>配布物の完全性（改ざん防止）</li>
<li>社内ネットワーク接続経路</li>
</ul>
<p>攻撃経路は主に次です。</p>
<ol>
<li>悪意ある PR が workflow を悪用</li>
<li>Marketplace Action の supply chain 汚染</li>
<li>ランナー上に残る credential / build artifact</li>
<li>過剰な <code>GITHUB_TOKEN</code> 権限</li>
</ol>
<p>この4点を潰す設計が防御の中心になります。</p>
<h2 id="1-ランナーは使い捨てを前提にする">1. ランナーは「使い捨て」を前提にする</h2>
<p>長寿命ランナーは便利ですが、攻撃後の残留リスクが高いです。可能ならジョブ単位で破棄できる ephemeral 構成を採用します。</p>
<ul>
<li>Kubernetes + Actions Runner Controller</li>
<li>VMテンプレートから都度起動</li>
<li>ジョブ終了後に完全破棄</li>
</ul>
<p>少なくとも <code>/tmp</code> と workspace を確実に消去し、Docker layer cache の共有範囲を制御してください。</p>
<h2 id="2-workflow-権限を最小化する">2. workflow 権限を最小化する</h2>
<p><code>permissions: write-all</code> は禁止レベルです。workflowごとに最小権限を明記します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">permissions</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">contents</span>: <span style="color:#ae81ff">read</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pull-requests</span>: <span style="color:#ae81ff">write</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">id-token</span>: <span style="color:#ae81ff">write</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>特に <code>id-token: write</code> は OIDC 連携に必要ですが、不要ジョブで許可しないこと。権限漏れがクラウド侵害に直結します。</p>
<h2 id="3-fork-pr-の実行ポリシーを分離する">3. fork PR の実行ポリシーを分離する</h2>
<p>外部 fork からの PR で secrets を使うジョブを実行しない設計が必須です。</p>
<p>推奨:</p>
<ul>
<li><code>pull_request</code> イベント: テストのみ、secrets 無し</li>
<li><code>pull_request_target</code>: 原則禁止、必要なら厳格レビュー</li>
<li>デプロイ系は <code>push</code>（保護ブランチ）だけ</li>
</ul>
<p>また、<code>workflow_run</code> を使って「検証済み成果物だけを次段へ渡す」2段構成にすると安全性が上がります。</p>
<h2 id="4-action-の固定と検証">4. Action の固定と検証</h2>
<p><code>uses: actions/checkout@v4</code> のようなタグ指定だけでは、将来更新の影響を受けます。高リスク工程では commit SHA 固定を検討します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@8ade135a...</span> <span style="color:#75715e"># SHA pin</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>加えて、許可済み Action の allowlist を組織ポリシー化します。無制限に Marketplace Action を使わせると監査不能になります。</p>
<h2 id="5-secrets-管理は-oidc-中心へ">5. Secrets 管理は OIDC 中心へ</h2>
<p>長期固定鍵を GitHub Secrets に置く運用は、漏洩時の被害が大きいです。クラウド連携は OIDC フェデレーションに移行し、短命トークンを発行します。</p>
<p>利点:</p>
<ul>
<li>鍵配布不要</li>
<li>期限短い</li>
<li>リポジトリ/ブランチ条件で絞れる</li>
</ul>
<p>AWS なら role trust policy に <code>sub</code> 条件を入れ、「main ブランチの release workflow だけ許可」といった制御が可能です。</p>
<h2 id="6-ネットワーク分離と出口制御">6. ネットワーク分離と出口制御</h2>
<p>セルフホストランナーが社内フラットネットワークに直結している構成は危険です。ランナー専用サブネットを作り、次を実施します。</p>
<ul>
<li>egress allowlist（必要ドメインのみ）</li>
<li>社内DBへの直接接続禁止</li>
<li>管理プレーンと実行プレーン分離</li>
</ul>
<p>「CIだから社内に近いほど便利」は短期的発想です。侵害前提で最小到達範囲に設計します。</p>
<h2 id="7-監査ログと改ざん検知">7. 監査ログと改ざん検知</h2>
<p>必要なログ:</p>
<ul>
<li>workflow 実行者</li>
<li>使用ランナーID</li>
<li>取得したクラウド権限</li>
<li>成果物ハッシュ</li>
</ul>
<p>さらに、リリース成果物に SBOM と署名（cosign / sigstore）を付与し、配布前検証を自動化します。これで supply chain の追跡性が大きく向上します。</p>
<h2 id="8-実装チェックリストそのまま使える">8. 実装チェックリスト（そのまま使える）</h2>
<ul>
<li><input disabled="" type="checkbox"> セルフホストランナーは ephemeral 運用</li>
<li><input disabled="" type="checkbox"> workflow permissions を最小権限化</li>
<li><input disabled="" type="checkbox"> fork PR と deploy workflow を分離</li>
<li><input disabled="" type="checkbox"> 高リスク Action は SHA pin</li>
<li><input disabled="" type="checkbox"> OIDC による短命認証へ移行</li>
<li><input disabled="" type="checkbox"> ネットワーク egress 制限</li>
<li><input disabled="" type="checkbox"> artifact 署名と SBOM 生成</li>
<li><input disabled="" type="checkbox"> 監査ログを90日以上保持</li>
</ul>
<h2 id="9-段階導入プラン4週間">9. 段階導入プラン（4週間）</h2>
<ul>
<li>Week1: 権限棚卸し、<code>write-all</code> 排除</li>
<li>Week2: fork PR ポリシー分離、allowlist導入</li>
<li>Week3: OIDC移行、固定鍵削減</li>
<li>Week4: ephemeral runner 化、署名/SBOM実装</li>
</ul>
<p>一気に変えると運用停止リスクがあるため、週単位で区切るのが現実的です。</p>
<h2 id="まとめ">まとめ</h2>
<p>セルフホストランナーは強力ですが、セキュリティ設計を誤ると CI/CD が最も危険な入口になります。ポイントは「最小権限」「使い捨て」「短命認証」「監査可能性」の4つです。</p>
<p>まずは <code>permissions</code> の最小化と fork PR 分離から始めてください。ここを押さえるだけでも、供給網リスクは大幅に下げられます。</p>
<h2 id="実運用での検知ルール例siem連携">実運用での検知ルール例（SIEM連携）</h2>
<p>防御策を実装しても、検知が弱いと侵害を見逃します。次のイベントを SIEM 側で高優先度アラート化してください。</p>
<ul>
<li>深夜帯の workflow 権限変更</li>
<li>普段使わないランナーラベルでのジョブ実行</li>
<li>release workflow で未知の Action 呼び出し</li>
<li>OIDC 経由で想定外クラウドロールを取得</li>
</ul>
<p>検知時には自動で <code>repository dispatch</code> を使い、該当リポジトリのデプロイを一時停止する仕組みを入れると被害拡大を防げます。</p>
<h2 id="インシデント対応runbookの最小構成">インシデント対応Runbookの最小構成</h2>
<p>供給網インシデントは初動が遅れると致命傷になります。Runbook には最低限次を含めます。</p>
<ol>
<li>影響範囲特定（対象リポジトリ、workflow、artifact）</li>
<li>該当 Secrets/Role の無効化</li>
<li>ランナー群の全廃棄と再構築</li>
<li>直近リリース成果物のハッシュ再検証</li>
<li>監査ログ保全と関係者通知</li>
</ol>
<p>この手順を平時に演習しておくことで、実際の障害時に迷いを減らせます。</p>
<h2 id="組織導入で効くポリシー">組織導入で効くポリシー</h2>
<ul>
<li>新規 workflow はセキュリティレビュー必須</li>
<li>Action 追加時はリスク評価テンプレート提出</li>
<li>重要リポジトリは branch protection + required review を強制</li>
<li>セルフホストランナーの管理責任者を明確化</li>
</ul>
<p>技術対策だけでは継続しません。責任分界とレビュー手順を運用ルール化することが、防御の持続性を高めます。</p>
<h2 id="まとめ運用視点">まとめ（運用視点）</h2>
<p>最終的に重要なのは「侵害されないこと」ではなく「侵害されても被害を限定し、速く復旧できること」です。セルフホストランナーを使うなら、最初からゼロトラスト前提で設計し、検知・対応まで含めた体制を整えてください。</p>
<h3 id="最後に">最後に</h3>
<p>現場では「便利だから後で固める」が最も危険です。セルフホストランナーは導入初日から最小権限と隔離を前提に設計し、定期監査で逸脱を戻す運用を続けてください。</p>
<h3 id="追加の運用tip">追加の運用Tip</h3>
<p>新しいリポジトリを作るたびに同じ議論をしないため、テンプレートリポジトリへ安全な workflow 雛形を同梱しておくと効果的です。初期状態を安全側に固定するだけで、運用負荷と事故率の両方を下げられます。</p>
<p>継続的な棚卸しを習慣化してください。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>GitHub Actions</category>
      <category>Security</category>
      <category>CI/CD</category>
      <category>DevSecOps</category>
    </item>
  </channel>
</rss>
