<?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>Platform Engineering on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/platform-engineering/</link>
    <description>Recent content in Platform Engineering on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Sat, 07 Mar 2026 09:05:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/platform-engineering/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Terraform→OpenTofu移行実践ガイド: 既存IaCを止めずに移行するエンタープライズ手順</title>
      <link>https://www.ai2core.com/posts/2026-03-07-terraform-opentofu-migration-enterprise-playbook/</link>
      <pubDate>Sat, 07 Mar 2026 09:05:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-07-terraform-opentofu-migration-enterprise-playbook/</guid>
      <description>Terraform資産をOpenTofuへ安全に移行するための、互換性確認、state管理、CI更新、段階的切替の実践手順を具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="terraformopentofu移行実践ガイド-既存iacを止めずに移行するエンタープライズ手順">Terraform→OpenTofu移行実践ガイド: 既存IaCを止めずに移行するエンタープライズ手順</h1>
<p>Terraformのライセンス変更以降、OpenTofuへ移行したいという相談は確実に増えています。とはいえ現場の本音は「理屈は分かるが、stateが壊れたら終わる」「本番を止めずに移行できるのか不安」です。</p>
<p>結論から言うと、移行は十分可能です。ただし「CLIを置き換えるだけ」で済むケースは限定的で、実際は <strong>providerバージョン整合・state lock・CI/CD・運用Runbook</strong> までまとめて整える必要があります。</p>
<p>本記事では、既にTerraformを本番運用しているチーム向けに、OpenTofuへ段階移行する実践手順をまとめます。</p>
<h2 id="1-移行方針を先に決める">1. 移行方針を先に決める</h2>
<p>最初に決めるべきは「一気に切り替えるか」「ワークスペース単位で段階移行するか」です。実務では次の方針が安全です。</p>
<ol>
<li>低リスク環境（dev/sandbox）から先行</li>
<li>本番は最終フェーズで移行</li>
<li>旧TerraformとOpenTofuを一定期間並行運用</li>
<li>ロールバック手順を文書化してから実施</li>
</ol>
<p>この順序を守るだけで、移行事故の大半を避けられます。</p>
<h2 id="2-互換性の棚卸し最重要">2. 互換性の棚卸し（最重要）</h2>
<p>まずは現状のIaC資産を棚卸しします。</p>
<ul>
<li>Terraformバージョン（例: 1.5.x / 1.6.x）</li>
<li>使用provider（AWS/Azure/GCP/Kubernetes等）</li>
<li>backend（S3 + DynamoDB lock、Terraform Cloud、GCSなど）</li>
<li>moduleの参照方式（registry / git / local）</li>
<li>CI実行環境（GitHub Actions, GitLab CI, Jenkins）</li>
</ul>
<h3 id="21-依存を固定化してから移行する">2.1 依存を固定化してから移行する</h3>
<p><code>.terraform.lock.hcl</code> を必ずコミットし、providerを固定します。移行時にproviderまで同時更新すると、差分原因の切り分けが困難になります。</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-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">terraform</span> {
</span></span><span style="display:flex;"><span>  required_version <span style="color:#f92672">=</span> &#34;&gt;<span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>.<span style="color:#ae81ff">6</span>.<span style="color:#ae81ff">0</span><span style="color:#960050;background-color:#1e0010">&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">required_providers</span> {
</span></span><span style="display:flex;"><span>    aws <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      source  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hashicorp/aws&#34;</span>
</span></span><span style="display:flex;"><span>      version <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;~&gt; 5.40&#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></code></pre></td></tr></table>
</div>
</div><p>移行フェーズでは「ツール差分」と「provider差分」を分離してください。</p>
<h2 id="3-ローカル検証の基本手順">3. ローカル検証の基本手順</h2>
<p>OpenTofuを導入したら、既存プロジェクトで次を実行します。</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 現状確認</span>
</span></span><span style="display:flex;"><span>terraform version
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># OpenTofu側確認</span>
</span></span><span style="display:flex;"><span>tofu version
</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>tofu init -upgrade<span style="color:#f92672">=</span>false
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 差分確認</span>
</span></span><span style="display:flex;"><span> tofu plan -out<span style="color:#f92672">=</span>tfplan
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>plan</code> が完全一致しない場合、いきなり適用してはいけません。主な原因は以下です。</p>
<ul>
<li>providerの暗黙更新</li>
<li>data sourceの評価タイミング差</li>
<li>標準関数・型変換の微差</li>
<li>module内部のversion制約</li>
</ul>
<p>差分は「構文差分」より「実行時評価差分」で出ることが多い点に注意してください。</p>
<h2 id="4-stateを守る設計壊したら復旧が重い">4. Stateを守る設計（壊したら復旧が重い）</h2>
<p>IaC移行の本質的リスクはstateです。ここを守るために、以下を移行前チェックリストに組み込みます。</p>
<ul>
<li>state backendのバックアップ取得</li>
<li>lock機構が有効か確認（DynamoDB等）</li>
<li><code>apply</code> 権限を移行担当者に限定</li>
<li>並列実行ジョブを停止（同時apply禁止）</li>
</ul>
<h3 id="41-s3-backendの例">4.1 S3 backendの例</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></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-hcl" data-lang="hcl"><span style="display:flex;"><span><span style="color:#66d9ef">terraform</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">backend</span> <span style="color:#e6db74">&#34;s3&#34;</span> {
</span></span><span style="display:flex;"><span>    bucket         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;prod-iac-state&#34;</span>
</span></span><span style="display:flex;"><span>    key            <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;network/prod.tfstate&#34;</span>
</span></span><span style="display:flex;"><span>    region         <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ap-northeast-1&#34;</span>
</span></span><span style="display:flex;"><span>    dynamodb_table <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;terraform-locks&#34;</span>
</span></span><span style="display:flex;"><span>    encrypt        <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</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>OpenTofuでもこのbackend構成は継続利用できます。重要なのは「誰がいつlockするか」を運用で統制することです。</p>
<h2 id="5-cicdをopentofu対応に置換">5. CI/CDをOpenTofu対応に置換</h2>
<p>CLI切替はローカルだけでは不十分です。実運用ではCIがapply主体です。</p>
<h3 id="51-github-actions移行例">5.1 GitHub Actions移行例</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">name</span>: <span style="color:#ae81ff">iac-plan</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">paths</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;infra/**&#34;</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">plan</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">defaults</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">run</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">working-directory</span>: <span style="color:#ae81ff">infra</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">uses</span>: <span style="color:#ae81ff">opentofu/setup-opentofu@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">tofu_version</span>: <span style="color:#ae81ff">1.8.5</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">tofu init -input=false</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">tofu validate</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">tofu plan -input=false -no-color</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>ポイントは、<code>tofu_version</code> を固定し、突然のツール更新を防ぐことです。</p>
<h3 id="52-planの品質ゲート">5.2 Planの品質ゲート</h3>
<ul>
<li><code>tofu fmt -check</code></li>
<li><code>tofu validate</code></li>
<li><code>tflint</code></li>
<li><code>checkov</code> や <code>tfsec</code> によるセキュリティ検査</li>
</ul>
<p>この4点をPRゲート化すると、移行直後の品質低下を防げます。</p>
<h2 id="6-段階移行の実践フロー">6. 段階移行の実践フロー</h2>
<p>本番で安全に移行するなら、次の順番が現実的です。</p>
<ol>
<li>dev workspaceをOpenTofuに切替</li>
<li>1週間運用し、差分・事故を記録</li>
<li>stg workspaceを切替</li>
<li>本番はメンテウィンドウで切替</li>
<li>切替後48時間は変更凍結（緊急以外apply禁止）</li>
</ol>
<p>移行で一番危険なのは「切替当日に通常の機能変更を混ぜる」ことです。必ず分離してください。</p>
<h2 id="7-よくある障害と対策">7. よくある障害と対策</h2>
<h3 id="71-plan-差分が毎回揺れる">7.1 <code>plan</code> 差分が毎回揺れる</h3>
<p>原因:</p>
<ul>
<li>data sourceに非決定的値（timestamp等）が混在</li>
<li>providerバージョン不統一</li>
</ul>
<p>対策:</p>
<ul>
<li>変動値をlocalsに隔離</li>
<li>provider lockを全環境で統一</li>
</ul>
<h3 id="72-import済み資源が再作成扱いになる">7.2 import済み資源が再作成扱いになる</h3>
<p>原因:</p>
<ul>
<li>module path変更</li>
<li>resource address変更（<code>count</code>→<code>for_each</code>）</li>
</ul>
<p>対策:</p>
<ul>
<li><code>moved</code> ブロックを使い論理移動を明示</li>
<li>必要時のみ <code>state mv</code> を計画的に実施</li>
</ul>
<h3 id="73-lock解放漏れでapplyが詰まる">7.3 lock解放漏れでapplyが詰まる</h3>
<p>原因:</p>
<ul>
<li>CI異常終了</li>
<li>手動中断</li>
</ul>
<p>対策:</p>
<ul>
<li>lock timeoutを定義</li>
<li>強制unlock手順をRunbook化</li>
<li>Slack通知でlock継続を可視化</li>
</ul>
<h2 id="8-運用runbookに必ず入れる項目">8. 運用Runbookに必ず入れる項目</h2>
<ul>
<li>OpenTofuバージョン更新手順</li>
<li>backend障害時の復旧手順</li>
<li>lock競合時の対応フロー</li>
<li>監査ログ（誰がいつapplyしたか）</li>
<li>ロールバック条件（差分件数・重大リスク判定）</li>
</ul>
<p>「ツール移行成功」は、CLIが動くことではなく、運用が回ることです。</p>
<h2 id="9-具体的な移行チェックリスト">9. 具体的な移行チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> <code>.terraform.lock.hcl</code> を全リポジトリで固定</li>
<li><input disabled="" type="checkbox"> OpenTofu版CIテンプレート作成</li>
<li><input disabled="" type="checkbox"> dev/stg/prodの順に移行計画作成</li>
<li><input disabled="" type="checkbox"> stateバックアップの自動化</li>
<li><input disabled="" type="checkbox"> lock競合監視の通知導入</li>
<li><input disabled="" type="checkbox"> apply実行権限を限定</li>
<li><input disabled="" type="checkbox"> 切替後の変更凍結ルールを明文化</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>TerraformからOpenTofuへの移行は、単純なCLI置換ではなく <strong>IaC運用基盤の再整備</strong> です。</p>
<ul>
<li>差分原因を分離して検証する</li>
<li>state保護を最優先に設計する</li>
<li>CIとRunbookをセットで更新する</li>
<li>段階移行でリスクを局所化する</li>
</ul>
<p>この4点を守れば、既存システムを止めずに移行できます。まずはdev環境で「同一plan再現性」を検証し、そこから本番へ広げるのが最短かつ安全です。</p>
<h2 id="付録-切替当日の実行チェック時系列">付録: 切替当日の実行チェック（時系列）</h2>
<p>移行当日は、作業手順よりも「順番管理」が事故防止に効きます。以下は実務で使える時系列テンプレートです。</p>
<ul>
<li><strong>T-30分</strong>: 変更凍結を全チームへ告知、未マージPRを棚卸し</li>
<li><strong>T-20分</strong>: <code>tofu version</code> と lockfile整合性確認、CI変数最終確認</li>
<li><strong>T-10分</strong>: backend到達確認、stateバックアップ取得</li>
<li><strong>T+0分</strong>: dev→stg→prodの順で <code>tofu plan</code> 実行（差分をレビュー）</li>
<li><strong>T+15分</strong>: 許容差分のみ <code>tofu apply</code> 実行、結果を監査ログへ転記</li>
<li><strong>T+30分</strong>: 主要監視（APIエラー率、レイテンシ、コスト）を15分監視</li>
</ul>
<p>さらに、切替時に最も有効なのは「中止判断ライン」を先に決めることです。例えば、<code>plan差分件数が想定の2倍以上</code> や <code>apply失敗が2連続</code> の場合は即時中断し、旧フローへ戻す、といった基準を明文化しておくと現場判断がぶれません。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>OpenTofu</category>
      <category>Terraform</category>
      <category>IaC</category>
      <category>DevOps</category>
      <category>Platform Engineering</category>
    </item>
    <item>
      <title>Kyvernoで始めるKubernetes Admission Policy実践: 事故を減らすポリシー設計プレイブック</title>
      <link>https://www.ai2core.com/posts/2026-03-06-kubernetes-admission-policy-kyverno-playbook/</link>
      <pubDate>Fri, 06 Mar 2026 09:05:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-06-kubernetes-admission-policy-kyverno-playbook/</guid>
      <description>KubernetesでKyvernoを使い、現場で運用可能なAdmission Policyを段階導入するための実装手順とトラブル対応を具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="kyvernoで始めるkubernetes-admission-policy実践-事故を減らすポリシー設計プレイブック">Kyvernoで始めるKubernetes Admission Policy実践: 事故を減らすポリシー設計プレイブック</h1>
<p>Kubernetes運用で一番つらい事故は、クラスタが壊れるよりも「本来防げたはずのミスがそのまま本番へ入る」ことです。たとえば、<code>latest</code> タグのイメージが本番に入り再現不能になる、<code>resources</code> 未設定でノードが詰まる、<code>privileged</code> コンテナが混入する。これらは人の注意力だけに依存すると必ず再発します。</p>
<p>そこで有効なのが Admission Policy（入場制御）です。本記事では <strong>Kyverno</strong> を使って、現場で本当に運用できるポリシー群を段階導入する手順をまとめます。単なる「denyの例」ではなく、監査→警告→強制の移行、例外管理、CI連携まで含めて解説します。</p>
<h2 id="1-なぜkyvernoなのか">1. なぜKyvernoなのか</h2>
<p>OPA Gatekeeper も強力ですが、Kyvernoは以下の特徴があり、初期導入が比較的スムーズです。</p>
<ul>
<li>YAML中心で書ける（Rego学習コストを後回しにしやすい）</li>
<li>validate / mutate / generate / verifyImages を一貫して扱える</li>
<li>PolicyReportにより違反可視化がしやすい</li>
<li>Pod SecurityやSupply Chain対策との相性が良い</li>
</ul>
<p>「まずルールを回し始める」目的なら、Kyvernoは現実的な選択肢です。</p>
<h2 id="2-先に決めるべき設計原則">2. 先に決めるべき設計原則</h2>
<p>導入前に、以下だけは先に決めておきます。</p>
<ol>
<li><strong>導入フェーズ</strong>: <code>Audit</code> → <code>Enforce</code> を基本にする</li>
<li><strong>責任分界</strong>: プラットフォームチームが共通ポリシー、各チームがアプリ固有例外</li>
<li><strong>例外の期限</strong>: 永久例外は禁止。期限付きで必ず棚卸し</li>
<li><strong>観測性</strong>: 違反数・対象Namespace・上位違反ルールをダッシュボード化</li>
</ol>
<p>この原則なしにルールだけ増やすと、運用が破綻します。</p>
<h2 id="3-最小導入手順3060分">3. 最小導入手順（30〜60分）</h2>
<h3 id="31-kyvernoのインストール">3.1 Kyvernoのインストール</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></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>helm repo add kyverno https://kyverno.github.io/kyverno/
</span></span><span style="display:flex;"><span>helm repo update
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>helm upgrade --install kyverno kyverno/kyverno <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -n kyverno --create-namespace <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set admissionController.replicas<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set backgroundController.replicas<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set cleanupController.replicas<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  --set reportsController.replicas<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>本番では可用性のため、admission/backgroundは最低2レプリカ推奨です。</p>
<h3 id="32-まずはauditモードで3ルール">3.2 まずはAuditモードで3ルール</h3>
<p>最初に効くルールは、次の3つです。</p>
<ul>
<li>イメージタグに <code>latest</code> を禁止</li>
<li>CPU/Memory requests/limits必須</li>
<li><code>privileged: true</code> を禁止</li>
</ul>
<p>例: <code>latest</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><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></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">apiVersion</span>: <span style="color:#ae81ff">kyverno.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">disallow-latest-tag</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">validationFailureAction</span>: <span style="color:#ae81ff">Audit</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">background</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">validate-image-tag</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">match</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">any</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">kinds</span>:
</span></span><span style="display:flex;"><span>                - <span style="color:#ae81ff">Pod</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">validate</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">message</span>: <span style="color:#e6db74">&#34;latestタグは禁止です。固定タグまたはdigestを使用してください。&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">foreach</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">list</span>: <span style="color:#e6db74">&#34;request.object.spec.containers&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">deny</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">conditions</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">any</span>:
</span></span><span style="display:flex;"><span>                  - <span style="color:#f92672">key</span>: <span style="color:#e6db74">&#34;{{ element.image }}&#34;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">operator</span>: <span style="color:#ae81ff">Matches</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">value</span>: <span style="color:#e6db74">&#34;.*:latest$&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="33-レポートで現状把握">3.3 レポートで現状把握</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></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>kubectl get policyreport -A
</span></span><span style="display:flex;"><span>kubectl get clusterpolicy
</span></span><span style="display:flex;"><span>kubectl describe clusterpolicy disallow-latest-tag
</span></span></code></pre></td></tr></table>
</div>
</div><p>導入直後は違反が大量に出るのが普通です。ここで「Kyvernoが厳しすぎる」と判断しないでください。違反は“見えていなかった負債”です。</p>
<h2 id="4-実務で効くルールセット具体例">4. 実務で効くルールセット（具体例）</h2>
<h3 id="41-リソース未設定防止">4.1 リソース未設定防止</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><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></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">apiVersion</span>: <span style="color:#ae81ff">kyverno.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">require-resources</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">validationFailureAction</span>: <span style="color:#ae81ff">Audit</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">check-resources</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">match</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">any</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">kinds</span>: [<span style="color:#e6db74">&#34;Pod&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">validate</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">message</span>: <span style="color:#e6db74">&#34;全コンテナにrequests/limitsを設定してください。&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">pattern</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>              - <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">requests</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;?*&#34;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;?*&#34;</span>
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">limits</span>:
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">cpu</span>: <span style="color:#e6db74">&#34;?*&#34;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#f92672">memory</span>: <span style="color:#e6db74">&#34;?*&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="42-特権設定防止">4.2 特権設定防止</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></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">apiVersion</span>: <span style="color:#ae81ff">kyverno.io/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">ClusterPolicy</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">disallow-privileged</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">validationFailureAction</span>: <span style="color:#ae81ff">Enforce</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">rules</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">name</span>: <span style="color:#66d9ef">no</span>-<span style="color:#ae81ff">privileged</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">match</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">any</span>:
</span></span><span style="display:flex;"><span>          - <span style="color:#f92672">resources</span>:
</span></span><span style="display:flex;"><span>              <span style="color:#f92672">kinds</span>: [<span style="color:#e6db74">&#34;Pod&#34;</span>]
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">validate</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">message</span>: <span style="color:#e6db74">&#34;privilegedコンテナは禁止です。&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">pattern</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">containers</span>:
</span></span><span style="display:flex;"><span>              - <span style="color:#f92672">securityContext</span>:
</span></span><span style="display:flex;"><span>                  <span style="color:#f92672">=(privileged)</span>: <span style="color:#e6db74">&#34;false&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="43-digest固定の推奨supply-chain">4.3 Digest固定の推奨（Supply Chain）</h3>
<p>本来は digest 固定が理想です。移行期は <code>Audit</code> で始め、違反率が下がってから <code>Enforce</code> に切り替えます。</p>
<h2 id="5-auditからenforceへ移行する基準">5. AuditからEnforceへ移行する基準</h2>
<p>「何となく」で切り替えると炎上します。次の客観指標を使うと安全です。</p>
<ul>
<li>直近14日で対象ポリシー違反率が5%未満</li>
<li>主要Namespace（prod/stg/shared）で違反ゼロ</li>
<li>例外申請フロー（Issue/PRテンプレート）が整備済み</li>
<li>当番がトラブル時に切り戻し手順を理解している</li>
</ul>
<p><code>validationFailureAction</code> を一括で上げるのではなく、ルール単位・Namespace単位で段階化するのがポイントです。</p>
<h2 id="6-例外運用のテンプレート">6. 例外運用のテンプレート</h2>
<p>ポリシー導入で最も壊れるのは「例外管理」です。おすすめは以下。</p>
<ul>
<li>例外は <code>PolicyException</code> で明示</li>
<li>期限（例: 14日）を必須にする</li>
<li>チケット番号をannotationで必須化</li>
<li>期限切れを毎日バッチで通知</li>
</ul>
<p>例外YAMLに <code>expires</code> と <code>owner</code> を必須化すると、放置率が一気に下がります。</p>
<h2 id="7-ciに組み込んで本番前に落とす">7. CIに組み込んで“本番前に落とす”</h2>
<p>クラスタ投入時に拒否されるより、PR段階で検知される方が開発体験は良いです。<code>kyverno-cli</code> をCIで実行します。</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>kyverno apply policies/ -r manifests/ --audit-warn
</span></span></code></pre></td></tr></table>
</div>
</div><p>GitHub Actions例:</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">name</span>: <span style="color:#ae81ff">Validate manifests with Kyverno</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">    kyverno version
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    kyverno apply ./policies -r ./k8s --audit-warn</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>これで「マージ後に初めて失敗する」パターンを減らせます。</p>
<h2 id="8-よくある障害と対処">8. よくある障害と対処</h2>
<h3 id="症状1-正常なpodまで拒否される">症状1: 正常なPodまで拒否される</h3>
<ul>
<li>match条件が広すぎる可能性</li>
<li><code>exclude</code> で <code>kube-system</code> や監視系を一時除外</li>
<li>まずAuditで影響範囲を確認してからEnforceへ</li>
</ul>
<h3 id="症状2-admission-timeoutでデプロイ遅延">症状2: Admission timeoutでデプロイ遅延</h3>
<ul>
<li>Kyvernoコンポーネントのリソース不足を確認</li>
<li>policy数が多い場合、ルール統合とmatch最適化</li>
<li>API Serverとのネットワーク遅延確認</li>
</ul>
<h3 id="症状3-レポートは出るが改善が進まない">症状3: レポートは出るが改善が進まない</h3>
<ul>
<li>違反上位3ルールに絞ってKPI化</li>
<li>チーム別に違反件数を見える化</li>
<li>例外を期限付きに強制</li>
</ul>
<h2 id="9-運用を続けるためのダッシュボード指標">9. 運用を続けるためのダッシュボード指標</h2>
<p>最低限、以下を可視化してください。</p>
<ul>
<li>ルール別違反件数（7日移動平均）</li>
<li>Namespace別違反件数</li>
<li><code>Audit</code> と <code>Enforce</code> の比率</li>
<li>期限切れ例外の件数</li>
<li>deploy失敗要因のうちpolicy起因の割合</li>
</ul>
<p>これを見ないと、ポリシーは「導入しただけ」で止まります。</p>
<h2 id="10-実践ロードマップ最初の4週間">10. 実践ロードマップ（最初の4週間）</h2>
<ul>
<li><strong>Week 1</strong>: Kyverno導入、3ルールをAuditで開始</li>
<li><strong>Week 2</strong>: 違反上位を改善、例外テンプレート導入</li>
<li><strong>Week 3</strong>: 一部NamespaceでEnforce化</li>
<li><strong>Week 4</strong>: CI連携完了、SLOに違反率を組み込み</li>
</ul>
<p>4週間で「人頼みのレビュー文化」から「仕組みで防ぐ文化」へ移行できます。</p>
<h2 id="まとめ">まとめ</h2>
<p>Kyverno導入の本質は、Kubernetesを縛ることではなく、<strong>再発するミスを設計で減らすこと</strong>です。</p>
<ul>
<li>最初はAuditで可視化</li>
<li>指標を持って段階的にEnforce</li>
<li>例外は期限付きで管理</li>
<li>CIに前倒し検知を組み込む</li>
</ul>
<p>この4点を守れば、ポリシーは“開発を止める壁”ではなく“事故を減らすガードレール”になります。まずは <code>latest</code> 禁止とリソース必須の2ルールから始めて、違反データを見ながら育てていきましょう。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Kubernetes</category>
      <category>Kyverno</category>
      <category>Security</category>
      <category>Platform Engineering</category>
      <category>Policy as Code</category>
    </item>
    <item>
      <title>Kubernetesキャパシティ設計実践：HPA/VPA/Cluster Autoscalerを衝突させない運用術</title>
      <link>https://www.ai2core.com/posts/2026-03-03-kubernetes-hpa-vpa-capacity-tuning/</link>
      <pubDate>Tue, 03 Mar 2026 09:10:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-03-kubernetes-hpa-vpa-capacity-tuning/</guid>
      <description>Kubernetesのスケーリング機構を本番で安定運用するための設計、メトリクス選定、調整手順、障害時の確認ポイントを解説。</description>
      <content:encoded><![CDATA[<h1 id="kubernetesキャパシティ設計実践hpavpacluster-autoscalerを衝突させない運用術">Kubernetesキャパシティ設計実践：HPA/VPA/Cluster Autoscalerを衝突させない運用術</h1>
<p>Kubernetes は「自動でスケールするから安心」と思われがちですが、実運用では逆です。HPA、VPA、Cluster Autoscaler（CA）の設定が噛み合わないと、スケールアウトと再スケジューリングが衝突し、レイテンシ悪化やコスト増大を引き起こします。</p>
<p>本記事では、3つのオートスケーリング機構を同時運用する際の設計ポイントを、障害対応目線で整理します。</p>
<h2 id="1-役割分担を明確にする">1. 役割分担を明確にする</h2>
<p>まず前提として、各コンポーネントの責務を固定します。</p>
<ul>
<li>HPA: Pod 数を短期的に増減</li>
<li>VPA: Pod あたりの requests/limits を中長期で最適化</li>
<li>CA: ノード数を増減</li>
</ul>
<p>この役割分担が曖昧だと、同じ問題を複数レイヤーで同時に解こうとして不安定化します。特に Web/API ワークロードでは、<strong>HPA を主軸、VPA は recommendation 中心</strong>で始めるのが安全です。</p>
<h2 id="2-requestslimits-が崩れていると全て失敗する">2. requests/limits が崩れていると全て失敗する</h2>
<p>HPA の CPU 指標は requests 基準で計算されます。requests が不正確だと、HPA の判断もズレます。最初にやるべきは次です。</p>
<ol>
<li>過去 2 週間の実使用量を可視化</li>
<li>p95 使用量を requests の初期値に設定</li>
<li>limits は requests の 1.5〜2 倍で開始</li>
</ol>
<p>極端に低い requests は「見かけの高負荷」を作り、不要スケールを誘発します。逆に高すぎる requests は CA の過剰増設を招きます。</p>
<h2 id="3-hpa-指標選定の実践">3. HPA 指標選定の実践</h2>
<p>CPU だけで運用すると、I/O 待ちや外部 API 待ちのボトルネックを見逃します。推奨は複合指標です。</p>
<ul>
<li>CPU Utilization（基本）</li>
<li>メモリ使用率（リーク監視）</li>
<li>RPS あたりレイテンシ（SLO 接続）</li>
<li>Queue 長（非同期処理）</li>
</ul>
<p><code>autoscaling/v2</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><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></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">apiVersion</span>: <span style="color:#ae81ff">autoscaling/v2</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">HorizontalPodAutoscaler</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api-hpa</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">scaleTargetRef</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">apps/v1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Deployment</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">name</span>: <span style="color:#ae81ff">api</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">minReplicas</span>: <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">maxReplicas</span>: <span style="color:#ae81ff">30</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">metrics</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">Resource</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">resource</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">name</span>: <span style="color:#ae81ff">cpu</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">target</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">type</span>: <span style="color:#ae81ff">Utilization</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">averageUtilization</span>: <span style="color:#ae81ff">60</span>
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">Pods</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">pods</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">metric</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">name</span>: <span style="color:#ae81ff">http_requests_per_second</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">target</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">type</span>: <span style="color:#ae81ff">AverageValue</span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">averageValue</span>: <span style="color:#e6db74">&#34;80&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="4-スケール挙動を安定化する">4. スケール挙動を安定化する</h2>
<p>HPA は設定次第で「増えすぎ・減りすぎ」を起こします。<code>behavior</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></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">behavior</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">scaleUp</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">stabilizationWindowSeconds</span>: <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">policies</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">Percent</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">value</span>: <span style="color:#ae81ff">100</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">periodSeconds</span>: <span style="color:#ae81ff">60</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">scaleDown</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">stabilizationWindowSeconds</span>: <span style="color:#ae81ff">300</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">policies</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">Percent</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">value</span>: <span style="color:#ae81ff">20</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">periodSeconds</span>: <span style="color:#ae81ff">60</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>スケールアップは素早く、スケールダウンは慎重に、が基本です。障害時の復旧速度と平常時コストのバランスを取りやすくなります。</p>
<h2 id="5-vpa-の安全な導入順序">5. VPA の安全な導入順序</h2>
<p>VPA をいきなり <code>Auto</code> で入れると、Pod 再作成が頻発し、ピーク時間帯に影響することがあります。次の順序を推奨します。</p>
<ol>
<li><code>Off</code> で recommendation だけ収集</li>
<li>2〜4週間データ蓄積</li>
<li>非クリティカルなバッチ系から <code>Auto</code> 開始</li>
<li>API 系は <code>Initial</code> または手動反映を継続</li>
</ol>
<p>この段階導入にすると、VPA が既存 SLO を壊すリスクを抑えられます。</p>
<h2 id="6-hpa-と-vpa-の衝突回避">6. HPA と VPA の衝突回避</h2>
<p>同一 Deployment で CPU/Memory の両方を HPA と VPA が同時に強く制御すると、調整ループが競合します。現場では次の運用が現実的です。</p>
<ul>
<li>HPA: レプリカ数を制御（主）</li>
<li>VPA: requests 最適化（従）</li>
<li>クリティカルサービスは VPA recommendation を人間レビューして反映</li>
</ul>
<p>また、メモリで OOM が起こるサービスは、HPA より先にアプリ側リーク調査を優先します。スケールで隠すと後で必ず再発します。</p>
<h2 id="7-cluster-autoscaler-の盲点">7. Cluster Autoscaler の盲点</h2>
<p>CA は Pending Pod を見てノードを増やしますが、以下の条件で期待通り動きません。</p>
<ul>
<li>PDB が厳しすぎて eviction できない</li>
<li>NodeSelector/taint 制約で配置先がない</li>
<li>requests が過大で BinPacking 不可能</li>
</ul>
<p>スケールしない時の確認手順を runbook 化しておくと、夜間障害で迷いません。</p>
<ol>
<li>Pending Pod の events を確認</li>
<li>CA logs で scale-up decision を確認</li>
<li>node group の上限値と quota を確認</li>
<li>Pod 制約（affinity/taint/tolerations）を確認</li>
</ol>
<h2 id="8-典型障害シナリオと対処">8. 典型障害シナリオと対処</h2>
<h3 id="シナリオa-突発トラフィックで-5xx-増加">シナリオA: 突発トラフィックで 5xx 増加</h3>
<ul>
<li>兆候: CPU 90%超、pod 起動待ち</li>
<li>対処: HPA minReplicas を一時引き上げ、イメージ pull 最適化、readiness 調整</li>
<li>恒久策: 予測ピーク前の scheduled scaling を導入</li>
</ul>
<h3 id="シナリオb-コスト急増">シナリオB: コスト急増</h3>
<ul>
<li>兆候: ノード数だけ増えて利用率低い</li>
<li>対処: requests 見直し、scaleDown stabilization 調整、CA consolidate 有効化</li>
<li>恒久策: ワークロード別の node pool 分離</li>
</ul>
<h3 id="シナリオc-断続的なタイムアウト">シナリオC: 断続的なタイムアウト</h3>
<ul>
<li>兆候: CPU 余裕あり、レイテンシ悪化</li>
<li>対処: 外部依存（DB/Redis/API）をトレースで確認</li>
<li>恒久策: HPA 指標に queue 長・レイテンシを追加</li>
</ul>
<h2 id="9-実運用のメトリクスセット">9. 実運用のメトリクスセット</h2>
<p>運用ダッシュボードには、最低限次を置きます。</p>
<ul>
<li>HPA current/desired replicas</li>
<li>Pod 起動時間（image pull + readiness）</li>
<li>Pod Pending 数と理由</li>
<li>Node 利用率（CPU/Memory）</li>
<li>CA scale up/down イベント数</li>
<li>5xx 率と p95 latency</li>
</ul>
<p>このセットがあると「スケーラが原因か、アプリが原因か」を 5 分以内で切り分けられます。</p>
<h2 id="10-段階的チューニング手順4週間">10. 段階的チューニング手順（4週間）</h2>
<h3 id="week-1-ベースライン計測">Week 1: ベースライン計測</h3>
<ul>
<li>現行 requests/limits と実使用量を比較</li>
<li>HPA 閾値を仮設定（CPU 60%）</li>
</ul>
<h3 id="week-2-振動抑制">Week 2: 振動抑制</h3>
<ul>
<li>behavior 追加</li>
<li>scaleDown window を 300s 前後で調整</li>
</ul>
<h3 id="week-3-指標追加">Week 3: 指標追加</h3>
<ul>
<li>RPS/queue 指標を導入</li>
<li>アラートを SLO 連動へ変更</li>
</ul>
<h3 id="week-4-コスト最適化">Week 4: コスト最適化</h3>
<ul>
<li>idle 時の minReplicas 見直し</li>
<li>node pool の統廃合</li>
</ul>
<p>この 4 週間を回すだけでも、無駄スケールと障害時復旧の両方が改善します。</p>
<h2 id="11-導入チェックリスト">11. 導入チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> requests/limits が実使用量に基づいている</li>
<li><input disabled="" type="checkbox"> HPA に <code>behavior</code> を設定済み</li>
<li><input disabled="" type="checkbox"> VPA は recommendation 期間を経て適用している</li>
<li><input disabled="" type="checkbox"> CA の上限値・quota・制約を定期点検している</li>
<li><input disabled="" type="checkbox"> SLO 指標とスケーリング指標を接続している</li>
<li><input disabled="" type="checkbox"> 典型障害シナリオの runbook がある</li>
</ul>
<p>Kubernetes のスケーリングは、機能を有効にするだけでは安定しません。役割分担、指標設計、調整ループの制御、そして runbook をセットで整えることで、初めて「自動化が味方になる」状態を作れます。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Kubernetes</category>
      <category>HPA</category>
      <category>VPA</category>
      <category>SRE</category>
      <category>Platform Engineering</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>
  </channel>
</rss>
