<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>ChengQing</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://www.realks.com/</id>
  <link href="https://www.realks.com/" rel="alternate"/>
  <link href="https://www.realks.com/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, ChengQing</rights>
  <subtitle>不断学习，不断进步</subtitle>
  <title>CQ的笔记</title>
  <updated>2026-06-21T02:30:00.000Z</updated>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="DevOps" scheme="https://www.realks.com/categories/DevOps/"/>
    <category term="CI/CD" scheme="https://www.realks.com/categories/DevOps/CI-CD/"/>
    <category term="Jenkins" scheme="https://www.realks.com/tags/Jenkins/"/>
    <category term="CI/CD" scheme="https://www.realks.com/tags/CI-CD/"/>
    <category term="Platform Engineering" scheme="https://www.realks.com/tags/Platform-Engineering/"/>
    <category term="GitHub Actions" scheme="https://www.realks.com/tags/GitHub-Actions/"/>
    <category term="Migration" scheme="https://www.realks.com/tags/Migration/"/>
    <content>
      <![CDATA[<h1 id="从-Jenkins-迁移到-GitHub-Actions：决策框架与方案对比"><a href="#从-Jenkins-迁移到-GitHub-Actions：决策框架与方案对比" class="headerlink" title="从 Jenkins 迁移到 GitHub Actions：决策框架与方案对比"></a>从 Jenkins 迁移到 GitHub Actions：决策框架与方案对比</h1><blockquote><p>本文是《统一 CI&#x2F;CD 流水线治理》系列第四篇，也是最后一篇。前三篇分别介绍了为什么要统一管理、Jenkins 如何实现、GitHub Actions 如何实现。本文的目的是帮助你做出有依据的决策：<strong>要不要迁移，以及如何迁移</strong>。本文来自一个覆盖 500+ 仓库、运行 2 年以上的生产实践——迁移决策不是纸上谈兵，而是真实发生过的权衡。</p></blockquote><hr><span id="more"></span><h2 id="一、先说结论：Jenkins-Shared-Library-没有错"><a href="#一、先说结论：Jenkins-Shared-Library-没有错" class="headerlink" title="一、先说结论：Jenkins Shared Library 没有错"></a>一、先说结论：Jenkins Shared Library 没有错</h2><p>在讨论迁移之前，必须明确一件事：<strong>Jenkins Shared Library 是成熟的、经过验证的方案</strong>。大量组织基于它稳定运行多年，它解决了统一 CI&#x2F;CD 治理的核心问题，有完整的生态支持。</p><p>迁移本身有真实的成本：</p><ul><li><strong>重写成本</strong>：Jenkins Groovy 逻辑需要翻译成 GitHub Actions YAML + shell，不是简单的格式转换</li><li><strong>培训成本</strong>：团队需要重新学习 GitHub Actions 的概念和调试方式</li><li><strong>验证成本</strong>：需要并行运行两套流水线对比结果，确保没有行为差异</li><li><strong>历史数据</strong>：Jenkins 的 build 历史、artifact、部署记录无法迁移</li><li><strong>500+ 仓库的迁移规模</strong>：500 个仓库的迁移不是一次 sprint 能完成的，需要以月到年为单位规划</li></ul><p><strong>不要为了迁移而迁移。</strong> 本文的目的是给出有依据的决策框架，而不是推销迁移。</p><hr><h2 id="二、两种方案的本质差异（深度对比）"><a href="#二、两种方案的本质差异（深度对比）" class="headerlink" title="二、两种方案的本质差异（深度对比）"></a>二、两种方案的本质差异（深度对比）</h2><h3 id="2-1-流水线结构：动态-vs-静态"><a href="#2-1-流水线结构：动态-vs-静态" class="headerlink" title="2.1 流水线结构：动态 vs 静态"></a>2.1 流水线结构：动态 vs 静态</h3><p>这是两种方案最本质的技术差异。</p><p><strong>Jenkins：运行时动态结构</strong></p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// JenkinsStageGenerator：stage 结构在运行时决定</span></span><br><span class="line"><span class="type">void</span> run(Map config, String vaultToken) &#123;</span><br><span class="line">    stage(<span class="string">&#x27;Lint&#x27;</span>) &#123; ... &#125;  <span class="comment">// 总是运行</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (config.jobs.find &#123; it.name == <span class="string">&#x27;unit-test&#x27;</span> &#125;) &#123;</span><br><span class="line">        stage(<span class="string">&#x27;Unit Test&#x27;</span>) &#123; ... &#125;  <span class="comment">// 条件运行</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 业务仓库可以在 config.yaml 中声明任意数量的自定义 stage</span></span><br><span class="line">    config.jobs.findAll &#123; it.name.startsWith(<span class="string">&#x27;custom-&#x27;</span>) &#125;.each &#123; job -&gt;</span><br><span class="line">        stage(job.name) &#123; runCustomJob(job) &#125;  <span class="comment">// 动态 stage</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Jenkins UI 中，没有单元测试的仓库根本不显示 “Unit Test” stage。</p><p><strong>GitHub Actions：编译时静态结构</strong></p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># workflow YAML 在解析时就固定了 job 结构</span></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">unit-test:</span></span><br><span class="line">    <span class="attr">if:</span> <span class="string">$&#123;&#123;</span> <span class="string">needs.config.outputs.has_unit_test</span> <span class="string">==</span> <span class="string">&#x27;true&#x27;</span> <span class="string">&#125;&#125;</span>  <span class="comment"># 只能跳过，不能&quot;不存在&quot;</span></span><br><span class="line">    <span class="string">...</span></span><br></pre></td></tr></table></figure><p>GitHub Actions 能跳过 job，但 job 始终出现在 workflow 定义中。你无法在 YAML 中声明”如果条件满足才有这个 job”。</p><p><strong>500+ 仓库规模下的实际影响：</strong> 对 95% 的业务场景，”可以跳过”和”不存在”没有区别。但在 500+ 仓库规模下，如果有几十个仓库需要声明任意数量的自定义 stage（数据库迁移、E2E 测试、多阶段部署），Jenkins 方案天然支持，GitHub Actions 需要用 matrix 或其他 workaround，且 UI 体验差距明显。</p><p><strong>实际数据：</strong> 在 500+ 仓库的平台中，大约 5-10% 的仓库使用了自定义 stage，这部分是迁移最复杂的场景——不是不能迁，而是迁移后的 YAML 体积会显著膨胀，且动态性降低。</p><h3 id="2-2-凭证安全模型：静态-vs-动态"><a href="#2-2-凭证安全模型：静态-vs-动态" class="headerlink" title="2.2 凭证安全模型：静态 vs 动态"></a>2.2 凭证安全模型：静态 vs 动态</h3><table><thead><tr><th>维度</th><th>Jenkins AppRole</th><th>GitHub Actions JWT&#x2F;OIDC</th></tr></thead><tbody><tr><td>静态凭证</td><td>SecretID 存在 Jenkins Credential Store</td><td>无静态凭证</td></tr><tr><td>凭证范围</td><td>所有 stage 共享同一 Vault token</td><td>每个 sub-workflow 独立认证</td></tr><tr><td>Token 生命周期</td><td>TTL 1小时（可配置）</td><td>5分钟 batch token，不可续期</td></tr><tr><td>泄漏可查</td><td>是（可从 Vault audit log 追踪）</td><td>batch token 不出现在 accessors</td></tr><tr><td>Groovy 代码读取凭证</td><td>理论上可以（<code>sh &#39;printenv&#39;</code>）</td><td>不适用</td></tr><tr><td>分支隔离</td><td>无（任何分支都能使用同一 SecretID）</td><td><code>@refs/heads/main</code> branch lock</td></tr></tbody></table><p><strong>500+ 仓库规模下的安全放大效应：</strong></p><p>AppRole 的核心风险在于 SecretID 全局共享。当平台覆盖 500 个仓库时：</p><ul><li>任意一个仓库的 Groovy Pipeline 理论上都能访问共享 SecretID</li><li>SecretID 轮换需要协调所有 500 个仓库同时不在认证——在高频 CI 环境中，轮换窗口极难控制</li><li>500 个仓库并发触发 AppRole 登录时，Vault 限流导致大量认证失败（见第三节）</li></ul><p>GitHub Actions 的 <code>job_workflow_ref</code> + <code>@refs/heads/main</code> 后缀强制要求所有 workflow 修改经过代码审查后才能访问 Vault，这个安全边界是由 Vault 的 JWT bound_claims 在认证层强制执行的，workflow 代码本身无法绕过：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">攻击者修改了 .github 仓库的 feature 分支中的 workflow 文件</span><br><span class="line">      │</span><br><span class="line">      ▼</span><br><span class="line">job_workflow_ref = &quot;OrgA/.github/.github/workflows/platform-ci-build.yml@refs/heads/attacker-branch&quot;</span><br><span class="line">      │</span><br><span class="line">      ▼</span><br><span class="line">Vault 验证：bound_claims 要求 @refs/heads/main</span><br><span class="line">      │</span><br><span class="line">      ▼</span><br><span class="line">✗ 不匹配 → 403 → 无法获取任何 secrets</span><br></pre></td></tr></table></figure><p><strong>500 个仓库，无一存储任何凭证</strong>——这是 JWT&#x2F;OIDC 方案在 500+ 规模下的核心安全价值。</p><h3 id="2-3-基础设施负担"><a href="#2-3-基础设施负担" class="headerlink" title="2.3 基础设施负担"></a>2.3 基础设施负担</h3><table><thead><tr><th>维度</th><th>Jenkins</th><th>GitHub Actions</th></tr></thead><tbody><tr><td>主节点维护</td><td>需要（JVM 调优、OOM 排查、插件管理）</td><td>不需要（GitHub 托管）</td></tr><tr><td>执行节点维护</td><td>Kubernetes 集群（Pod 调度、镜像更新）</td><td>self-hosted runner（如需内网资源）</td></tr><tr><td>插件生态</td><td>数百个插件，版本兼容性需要测试</td><td>Actions Marketplace，版本独立</td></tr><tr><td>升级影响</td><td>Jenkins 版本升级可能破坏 Pipeline</td><td>GitHub API 向后兼容，很少破坏性变更</td></tr></tbody></table><p><strong>500+ 仓库规模下的真实运维成本：</strong></p><p>Jenkins 的典型运维事件（已在 500+ 仓库规模实际发生）：</p><ul><li><strong>每季度 1-2 次</strong> Kubernetes Plugin 升级导致 Pod 调度失败，影响全量 CI（排查 2-4 小时，但全 500 仓库停摆）</li><li><strong>每年 1-2 次</strong> Jenkins 主节点 OOM（高峰期 100-200 个并发 Pipeline，JVM 堆耗尽，排查 + 恢复 4-8 小时）</li><li><strong>每月 1-2 次</strong> 某个插件与新版 Jenkins 不兼容，需要降级（排查 1-2 小时）</li><li><strong>Vault 限流</strong>：500+ 仓库同时 push 时（早高峰），AppRole 并发认证可能触发 Vault 限流，导致数百个 Pipeline 认证失败</li></ul><p>GitHub Actions self-hosted runner 的典型运维事件：</p><ul><li><strong>每季度 1 次</strong> runner 软件更新（5 分钟脚本自动完成，对运行中 job 无影响）</li><li><strong>偶发</strong> runner 网络超时（重跑 job 通常即可解决）</li><li><strong>runner 容量</strong>：500+ 仓库需要规划充足的 runner 数量，防止高峰排队</li></ul><h3 id="2-4-开发者体验"><a href="#2-4-开发者体验" class="headerlink" title="2.4 开发者体验"></a>2.4 开发者体验</h3><p>这个维度的差异对工程师的日常工作影响最大，在 500+ 仓库规模下，<strong>支持成本直接受此影响</strong>：</p><p><strong>Jenkins：</strong></p><ul><li>CI 结果在 Jenkins UI 中，PR 页面只显示”succeeded&#x2F;failed”</li><li>查看失败详情需要跳转到 Jenkins（可能需要额外登录）</li><li>日志在 Jenkins 的 Blue Ocean 或 Classic 界面，与代码审查流程割裂</li><li>失败原因经常是”某个 stage 失败”，需要逐层展开才能看到具体错误</li></ul><p><strong>GitHub Actions：</strong></p><ul><li>CI 结果直接显示在 PR 的 Checks 标签页</li><li>每个 step 的日志可以直接在 PR 页面展开查看</li><li>与代码审查、comments、assignee 完全一体化</li><li>失败的 step 有直接的”Re-run”按钮</li><li>Job Summary 页面可以展示结构化报告（配置解析结果、覆盖的模块列表等）</li></ul><p><strong>量化影响（500+ 仓库规模）：</strong></p><p>平台团队的调查显示，排查一次 CI 失败的平均时间：</p><ul><li>Jenkins：8-12 分钟（包括切换平台、找到对应 build、定位 stage）</li><li>GitHub Actions：3-5 分钟（直接在 PR 页面展开）</li></ul><p>在 500+ 仓库、每天数百次 CI 运行的规模下，如果每次 CI 失败平均少花 6 分钟排查，100 人工程团队每周节省约 50-100 人时。<strong>这个数字能直接支撑迁移投入的 ROI 计算。</strong></p><h3 id="2-5-配置合并能力"><a href="#2-5-配置合并能力" class="headerlink" title="2.5 配置合并能力"></a>2.5 配置合并能力</h3><table><thead><tr><th>维度</th><th>Jenkins Groovy Merger</th><th>GitHub Actions shell + yq</th></tr></thead><tbody><tr><td>类型安全</td><td>是（Groovy 强类型）</td><td>否（shell 字符串处理）</td></tr><tr><td>单元测试</td><td>是（JUnit + Spock）</td><td>较难（需要 bats 或 Python 测试）</td></tr><tr><td>复杂合并规则</td><td>容易（Groovy 列表操作）</td><td>需要手动处理 null、特殊字符、多行值</td></tr><tr><td>调试</td><td>println、Groovy console</td><td>echo + GITHUB_STEP_SUMMARY</td></tr></tbody></table><p><strong>shell 实现脆弱的场景：</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 当 pylint sourceSets 中包含路径含有空格时，tr &#x27;\n&#x27; &#x27; &#x27; 处理后的结果可能被 shell 分词</span></span><br><span class="line">SOURCES=$(yq <span class="string">&#x27;.check.pylint.sourceSets[]&#x27;</span> config.yaml | <span class="built_in">tr</span> <span class="string">&#x27;\n&#x27;</span> <span class="string">&#x27; &#x27;</span>)</span><br><span class="line"><span class="comment"># 如果某个路径是 &quot;src/my module&quot;，分词后会变成两个参数</span></span><br></pre></td></tr></table></figure><p><strong>500+ 仓库的边界情况放大效应：</strong> 在 500 个仓库中，任何 config.yaml 格式的边界情况都一定会出现——某个团队用了带空格的路径、某个团队用了多行 YAML 字符串、某个团队的字段值包含了 shell 特殊字符。Groovy 的类型系统在 500 个仓库的输入多样性下提供了更好的保证，而 shell + yq 方案需要更充分的测试覆盖。</p><h3 id="综合对比表"><a href="#综合对比表" class="headerlink" title="综合对比表"></a>综合对比表</h3><table><thead><tr><th>维度</th><th>Jenkins Shared Library</th><th>GitHub Actions Reusable Workflow</th></tr></thead><tbody><tr><td>流水线结构</td><td>动态（运行时决定）</td><td>静态（解析时固定）</td></tr><tr><td>凭证安全</td><td>AppRole（静态 SecretID，500 仓库共享）</td><td>JWT&#x2F;OIDC（无静态凭证）</td></tr><tr><td>基础设施负担</td><td>高（主节点 + K8s 集群）</td><td>低（仅 runner）</td></tr><tr><td>开发者体验</td><td>独立平台，上下文切换</td><td>与 GitHub PR 一体化</td></tr><tr><td>配置合并</td><td>类型安全，可单元测试</td><td>shell + yq，较脆弱</td></tr><tr><td>动态自定义 Stage</td><td>原生支持</td><td>不支持（只能跳过）</td></tr><tr><td>生态集成</td><td>数百个成熟插件</td><td>Marketplace 快速增长</td></tr><tr><td>500+ 规模 OOM 风险</td><td>高（Jenkins 主节点 JVM）</td><td>无（GitHub 托管计算）</td></tr></tbody></table><hr><h2 id="三、迁移的真实驱动力"><a href="#三、迁移的真实驱动力" class="headerlink" title="三、迁移的真实驱动力"></a>三、迁移的真实驱动力</h2><h3 id="驱动力一：代码托管平台统一（运维简化）"><a href="#驱动力一：代码托管平台统一（运维简化）" class="headerlink" title="驱动力一：代码托管平台统一（运维简化）"></a>驱动力一：代码托管平台统一（运维简化）</h3><p>维护两套系统（Jenkins + GitHub Enterprise）意味着：</p><ul><li>两套权限管理（新成员入职需要申请两个平台的访问权限）</li><li>两套审计日志（安全审计需要关联两个系统的记录）</li><li>两套事故响应（Jenkins 故障和 GitHub 故障是不同的响应流程）</li><li>两套监控配置</li></ul><p>在 500+ 仓库规模下，这个运维开销被进一步放大：两套系统的权限同步（离职人员需要从两个系统清除）、两套 CI 状态监控（某个仓库 CI 挂了，需要判断是 Jenkins 问题还是 GitHub 问题）。</p><p>如果代码已经全部迁移到 GitHub Enterprise，继续维护独立的 Jenkins 实例的边际价值在降低。</p><h3 id="驱动力二：凭证安全合规要求"><a href="#驱动力二：凭证安全合规要求" class="headerlink" title="驱动力二：凭证安全合规要求"></a>驱动力二：凭证安全合规要求</h3><p>某些合规框架（SOC 2 Type II、ISO 27001）对静态凭证有明确要求：</p><ul><li>必须定期轮换（通常 90 天）</li><li>必须有使用审计记录</li><li>必须限制访问范围</li></ul><p><strong>500+ 仓库规模下的 AppRole 合规挑战：</strong></p><p>AppRole 的 SecretID 满足这些要求需要额外的运维流程（定期轮换脚本、轮换审计）。在 500+ 仓库的高频 CI 环境中，90 天轮换意味着每次轮换都需要：</p><ol><li>选择低峰窗口（避免影响太多正在运行的 Pipeline）</li><li>更新 Jenkins Credential Store</li><li>验证新 SecretID 对所有 Org 的 Vault role 都生效</li><li>监控接下来 24 小时是否有认证失败</li></ol><p>JWT&#x2F;OIDC 的回答是”不存在可泄漏的长期凭证”——这在合规审计场景下是更简洁的答案。<strong>对 500+ 仓库的平台，这个答案可以减少每季度约 1-2 天的合规运维工作。</strong></p><h3 id="驱动力三：Jenkins-基础设施到达生命周期"><a href="#驱动力三：Jenkins-基础设施到达生命周期" class="headerlink" title="驱动力三：Jenkins 基础设施到达生命周期"></a>驱动力三：Jenkins 基础设施到达生命周期</h3><p>当出现以下信号时，迁移的 ROI 开始变得合理：</p><ul><li>Jenkins 主节点 OOM 频率从每年 1 次增加到每月 1 次</li><li>Kubernetes Plugin 升级在每次 Jenkins 版本更新后都需要手动回滚</li><li>负责维护 Jenkins 实例的工程师离职，知识传承困难</li><li>Jenkins LTS 版本接近 EOL（End of Life）</li></ul><p><strong>500+ 仓库规模的特殊信号：</strong></p><ul><li>早高峰 Vault 限流成为常态（每天早晨都需要手动清理或等待）</li><li>Jenkins 主节点需要持续的 JVM 调优（堆大小已超过 16GB 仍然 OOM）</li><li>Pod 调度失败的排查成本超过了新功能开发的投入</li></ul><p>这不是 Jenkins 的根本性问题，而是任何有状态基础设施在超规模使用时都会遭遇的生命周期问题。</p><h3 id="驱动力四：开发者生产力"><a href="#驱动力四：开发者生产力" class="headerlink" title="驱动力四：开发者生产力"></a>驱动力四：开发者生产力</h3><p>量化这个驱动力的方法：</p><ul><li>统计过去 3 个月，工程师在排查 CI 失败上花费的时间（通过 Jira ticket 或 Slack 记录）</li><li>统计平台团队在维护 Jenkins 基础设施上花费的时间</li><li>与迁移成本对比</li></ul><p><strong>500+ 仓库规模的具体估算：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">假设：</span><br><span class="line">  500 个仓库，平均每天每仓库 5 次 CI 运行 = 2500 次/天</span><br><span class="line">  CI 失败率 10%                            = 250 次失败/天</span><br><span class="line">  Jenkins 排查时间：10 分钟/次</span><br><span class="line">  GitHub Actions 排查时间：4 分钟/次</span><br><span class="line">  节省时间：6 分钟/次 × 250 次/天           = 25 小时/天</span><br><span class="line"></span><br><span class="line">年化节省（工作日 250 天）                    = 6250 小时/年</span><br></pre></td></tr></table></figure><p>即便实际数字只有估算的 1&#x2F;10，每年节省 625 小时也足以支持迁移投入。</p><hr><h2 id="四、不值得迁移的场景（重要）"><a href="#四、不值得迁移的场景（重要）" class="headerlink" title="四、不值得迁移的场景（重要）"></a>四、不值得迁移的场景（重要）</h2><h3 id="场景一：动态-Stage-生成是核心需求，且覆盖大量仓库"><a href="#场景一：动态-Stage-生成是核心需求，且覆盖大量仓库" class="headerlink" title="场景一：动态 Stage 生成是核心需求，且覆盖大量仓库"></a>场景一：动态 Stage 生成是核心需求，且覆盖大量仓库</h3><p>如果你的平台允许业务团队在 <code>config.yaml</code> 中声明任意的自定义 stage，且这个能力被 <strong>超过 10% 的仓库</strong> 广泛使用，GitHub Actions 无法原生复制这个功能。</p><p>具体例子：某个业务仓库声明了 10 个数据库迁移 stage，每个对应一个目标环境，stage 名称和数量在运行时从配置文件生成。Jenkins 的 <code>StageGenerator</code> 一行代码搞定，GitHub Actions 需要复杂的 matrix 配置，且在 UI 展示上差距明显。</p><p><strong>500+ 仓库下的评估方法：</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 检查有多少仓库使用了 custom- 开头的 stage（即动态 stage）</span></span><br><span class="line">gh repo list OrgA --<span class="built_in">limit</span> 1000 --json name \</span><br><span class="line">  | jq -r <span class="string">&#x27;.[].name&#x27;</span> \</span><br><span class="line">  | <span class="keyword">while</span> <span class="built_in">read</span> repo; <span class="keyword">do</span></span><br><span class="line">      <span class="keyword">if</span> gh api <span class="string">&quot;repos/OrgA/<span class="variable">$&#123;repo&#125;</span>/contents/.ci-config/config.yaml&quot;</span> --silent 2&gt;/dev/null \</span><br><span class="line">           | jq -r <span class="string">&#x27;.content&#x27;</span> | <span class="built_in">base64</span> -d | grep -q <span class="string">&#x27;custom-&#x27;</span>; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$&#123;repo&#125;</span>: 使用了自定义 stage&quot;</span></span><br><span class="line">      <span class="keyword">fi</span></span><br><span class="line">    <span class="keyword">done</span> | <span class="built_in">wc</span> -l</span><br></pre></td></tr></table></figure><p>如果输出超过 50 个仓库，动态 stage 迁移的工作量可能超过其他所有迁移工作之和。</p><h3 id="场景二：流水线配置合并逻辑极其复杂"><a href="#场景二：流水线配置合并逻辑极其复杂" class="headerlink" title="场景二：流水线配置合并逻辑极其复杂"></a>场景二：流水线配置合并逻辑极其复杂</h3><p>如果你的 <code>ConfigMerger</code> 已经积累了大量处理特殊场景的 Groovy 逻辑（递归合并、引用解析、条件覆盖），这些逻辑翻译成 shell + yq 不只是工作量大，还会降低可维护性和可测试性。</p><p>Groovy 的类型系统和 Jenkins 的测试工具链（<code>JenkinsPipelineUnit</code>）在这个场景下有不可替代的价值。<strong>在 500+ 仓库的输入多样性下，未经充分测试的 shell 配置解析可能引发大量偶发性问题。</strong></p><h3 id="场景三：深度依赖-Jenkins-特定插件"><a href="#场景三：深度依赖-Jenkins-特定插件" class="headerlink" title="场景三：深度依赖 Jenkins 特定插件"></a>场景三：深度依赖 Jenkins 特定插件</h3><p>以下插件目前没有成熟的 GitHub Actions 替代：</p><ul><li><strong>Build Failure Analyzer</strong>：自动分析失败原因并分类</li><li><strong>Performance Plugin</strong>：CI 性能趋势分析</li><li><strong>Warnings Next Generation</strong>：多工具警告汇聚和趋势分析</li><li><strong>Configuration as Code（JCasC）</strong>：Jenkins 配置版本化管理</li></ul><p>如果这些插件提供的功能是你的工作流核心，迁移会有明显的功能缺失。</p><h3 id="场景四：Jenkins-运行稳定，团队熟悉，没有明显痛点"><a href="#场景四：Jenkins-运行稳定，团队熟悉，没有明显痛点" class="headerlink" title="场景四：Jenkins 运行稳定，团队熟悉，没有明显痛点"></a>场景四：Jenkins 运行稳定，团队熟悉，没有明显痛点</h3><p>这是最重要的场景——<strong>如果现状运转良好，不要迁移</strong>。</p><p>评估”是否有痛点”的具体问题（针对 500+ 仓库规模）：</p><ol><li>过去 6 个月，Jenkins 基础设施故障导致多少次全量 CI 中断（影响 500 个仓库）？</li><li>过去一年，Vault 限流导致多少次 Pipeline 批量失败？</li><li>工程师是否在抱怨 CI 排查困难、需要切换平台？</li><li>凭证轮换是否按时完成，还是一再推迟？</li><li>Jenkins 主节点 JVM 堆是否持续满负荷运行？</li></ol><p>如果这五个问题的答案都是”没有”或”很少”，迁移的 ROI 很可能是负的。</p><h3 id="ROI-计算框架（500-仓库版）"><a href="#ROI-计算框架（500-仓库版）" class="headerlink" title="ROI 计算框架（500+ 仓库版）"></a>ROI 计算框架（500+ 仓库版）</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">迁移成本（一次性）：</span><br><span class="line">  - 平台工程师重写流水线代码：8-12 人周</span><br><span class="line">  - 500 个仓库迁移：</span><br><span class="line">    * 无自定义 stage 仓库（约 90%）：每仓库 30 分钟 = 225 人时</span><br><span class="line">    * 有自定义 stage 仓库（约 10%）：每仓库 4 小时 = 200 人时</span><br><span class="line">    * 合计：约 425 人时（约 53 人天）</span><br><span class="line">  - 并行验证周期：4-8 周（双轨运行成本）</span><br><span class="line">  - 培训（500 仓库的工程师）：1小时/人 × N 人</span><br><span class="line"></span><br><span class="line">持续收益（年化）：</span><br><span class="line">  - 减少 Jenkins 维护事件：6-10 次/年 × 4 小时/次 = 24-40 小时/年</span><br><span class="line">  - 减少 CI 故障排查时间：见第三节驱动力四估算</span><br><span class="line">  - 减少 Vault 限流影响：季度轮换 × 2 天/次 = 8 天/年</span><br><span class="line">  - 消除 Jenkins 主节点 OOM 风险</span><br><span class="line"></span><br><span class="line">迁移 ROI = 持续收益 / 迁移成本（以年计）</span><br></pre></td></tr></table></figure><hr><h2 id="五、如果决定迁移，如何降低风险（500-仓库策略）"><a href="#五、如果决定迁移，如何降低风险（500-仓库策略）" class="headerlink" title="五、如果决定迁移，如何降低风险（500+ 仓库策略）"></a>五、如果决定迁移，如何降低风险（500+ 仓库策略）</h2><h3 id="策略一：分波次迁移，而非大爆炸"><a href="#策略一：分波次迁移，而非大爆炸" class="headerlink" title="策略一：分波次迁移，而非大爆炸"></a>策略一：分波次迁移，而非大爆炸</h3><p>在 500+ 仓库规模下，”一次性迁移所有仓库”不是一个可执行的方案。分波次策略：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">第 0 阶段（1-2 个月）：搭建 GitHub Actions 平台框架</span><br><span class="line">  - 实现 platform-ci-core.yml 及所有 sub-workflow</span><br><span class="line">  - 确保与 .ci-config/config.yaml 接口完全兼容</span><br><span class="line">  - 针对 5-10 个内部测试仓库验证</span><br><span class="line"></span><br><span class="line">第 1 波（2-3 个月）：新仓库 + 低风险存量仓库（约 100 个）</span><br><span class="line">  - 所有新创建仓库默认 GitHub Actions</span><br><span class="line">  - 迁移活跃度低、业务影响小的存量仓库</span><br><span class="line">  - 目标：积累 6+ 个月的运行数据，发现并修复边界情况</span><br><span class="line"></span><br><span class="line">第 2 波（3-6 个月）：中等重要性仓库（约 250 个）</span><br><span class="line">  - 无自定义 stage 的标准仓库</span><br><span class="line">  - 并行运行 2 周后切断 Jenkins</span><br><span class="line"></span><br><span class="line">第 3 波（6-12 个月）：核心仓库 + 复杂仓库（约 150 个）</span><br><span class="line">  - 有自定义 stage 的仓库（需要单独评估）</span><br><span class="line">  - 高可见性的核心业务仓库（需要最充分的验证）</span><br><span class="line">  - 部分仓库可能永久保留 Jenkins（见共存方案）</span><br></pre></td></tr></table></figure><h3 id="策略二：并行运行期验证"><a href="#策略二：并行运行期验证" class="headerlink" title="策略二：并行运行期验证"></a>策略二：并行运行期验证</h3><p>在正式切换前，让 Jenkins 和 GitHub Actions 同时运行，对比每次 build 的结果：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 业务仓库的 ci.yml 临时配置</span></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="comment"># 新流水线（GitHub Actions）</span></span><br><span class="line">  <span class="attr">platform-gha:</span></span><br><span class="line">    <span class="attr">uses:</span> <span class="string">OrgA/.github/.github/workflows/platform-ci-core.yml@main</span></span><br><span class="line"></span><br><span class="line">  <span class="comment"># 旧流水线仍在 Jenkins 运行（通过 Jenkins GitHub Plugin 触发）</span></span><br><span class="line">  <span class="comment"># 两者结果在 PR 的 Checks 中并排显示</span></span><br></pre></td></tr></table></figure><p>只有当 GitHub Actions 结果稳定等于 Jenkins 结果至少 <strong>2 周、10 次以上 CI 运行</strong>后，才切断 Jenkins。</p><p><strong>500+ 仓库的注意事项：</strong> 不要在整个 500 个仓库上同时启动并行运行——双倍的 CI 负载会对 runner 容量和 Vault 造成额外压力。按波次启动并行运行，每波完成后再切断 Jenkins 连接，释放资源。</p><h3 id="策略三：保留-ci-config-config-yaml-接口不变（关键）"><a href="#策略三：保留-ci-config-config-yaml-接口不变（关键）" class="headerlink" title="策略三：保留 .ci-config/config.yaml 接口不变（关键）"></a>策略三：保留 <code>.ci-config/config.yaml</code> 接口不变（关键）</h3><p>这是降低业务团队迁移成本的最重要决策。如果 <code>.ci-config/config.yaml</code> 的结构保持兼容，业务团队的迁移工作只是：</p><ol><li>创建一个新的 <code>.github/workflows/ci.yml</code>（15 行）</li><li>删除旧的 <code>Jenkinsfile</code></li><li><code>.ci-config/config.yaml</code> <strong>完全不动</strong></li></ol><p><strong>业务团队的感知：</strong> CI 结果显示在不同的地方了，流水线快了，Jenkinsfile 不见了。其余一切不变。</p><p>在 500 个仓库中，这种无感迁移意味着每个仓库的迁移不需要专门的协调会议——平台团队准备好后，由业务团队在自己的节奏内完成两步操作。</p><h3 id="策略四：通过-Org-Variable-控制切换"><a href="#策略四：通过-Org-Variable-控制切换" class="headerlink" title="策略四：通过 Org Variable 控制切换"></a>策略四：通过 Org Variable 控制切换</h3><p>利用 GitHub Enterprise 的 Org Variable 机制，实现不修改业务仓库代码的路由控制：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 不同 Org 代表不同迁移阶段</span></span><br><span class="line"><span class="comment"># OrgA-Dev：新仓库优先接入 GitHub Actions（Dev 环境）</span></span><br><span class="line"><span class="comment"># OrgA-Stg：存量仓库逐步迁移（Staging 环境）</span></span><br><span class="line"><span class="comment"># OrgA：最后切换核心 prod 仓库（生产环境）</span></span><br></pre></td></tr></table></figure><p>Org Variable（<code>VAULT_ENV</code>、<code>VAULT_URL</code> 等）在不同 Org 中配置不同值，业务仓库的 <code>ci.yml</code> 代码完全相同，平台行为根据 Org 上下文自动路由。<strong>这是 500+ 仓库分阶段迁移的基础设施保证</strong>——你可以在不同 Org 处于不同迁移阶段时，平台仍然统一维护一套代码。</p><h3 id="策略五：批量检查迁移进度"><a href="#策略五：批量检查迁移进度" class="headerlink" title="策略五：批量检查迁移进度"></a>策略五：批量检查迁移进度</h3><p>在 500 个仓库的迁移过程中，需要工具化监控迁移状态：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"><span class="comment"># 检查各仓库的迁移状态：Jenkins only / GitHub Actions only / Both（并行）/ Neither</span></span><br><span class="line">ORG=<span class="string">&quot;OrgA&quot;</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;=== 迁移状态报告 ===&quot;</span></span><br><span class="line"></span><br><span class="line">gh repo list <span class="string">&quot;<span class="variable">$&#123;ORG&#125;</span>&quot;</span> --<span class="built_in">limit</span> 1000 --json name \</span><br><span class="line">  | jq -r <span class="string">&#x27;.[].name&#x27;</span> \</span><br><span class="line">  | <span class="keyword">while</span> <span class="built_in">read</span> repo; <span class="keyword">do</span></span><br><span class="line">      HAS_JENKINSFILE=$(gh api <span class="string">&quot;repos/<span class="variable">$&#123;ORG&#125;</span>/<span class="variable">$&#123;repo&#125;</span>/contents/Jenkinsfile&quot;</span> --silent 2&gt;/dev/null &amp;&amp; <span class="built_in">echo</span> <span class="string">&quot;yes&quot;</span> || <span class="built_in">echo</span> <span class="string">&quot;no&quot;</span>)</span><br><span class="line">      HAS_GHA=$(gh api <span class="string">&quot;repos/<span class="variable">$&#123;ORG&#125;</span>/<span class="variable">$&#123;repo&#125;</span>/contents/.github/workflows/ci.yml&quot;</span> --silent 2&gt;/dev/null &amp;&amp; <span class="built_in">echo</span> <span class="string">&quot;yes&quot;</span> || <span class="built_in">echo</span> <span class="string">&quot;no&quot;</span>)</span><br><span class="line">      HAS_CONFIG=$(gh api <span class="string">&quot;repos/<span class="variable">$&#123;ORG&#125;</span>/<span class="variable">$&#123;repo&#125;</span>/contents/.ci-config/config.yaml&quot;</span> --silent 2&gt;/dev/null &amp;&amp; <span class="built_in">echo</span> <span class="string">&quot;yes&quot;</span> || <span class="built_in">echo</span> <span class="string">&quot;no&quot;</span>)</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> [ <span class="string">&quot;<span class="variable">$&#123;HAS_JENKINSFILE&#125;</span>&quot;</span> = <span class="string">&quot;yes&quot;</span> ] &amp;&amp; [ <span class="string">&quot;<span class="variable">$&#123;HAS_GHA&#125;</span>&quot;</span> = <span class="string">&quot;no&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;[Jenkins only]    <span class="variable">$&#123;repo&#125;</span>&quot;</span></span><br><span class="line">      <span class="keyword">elif</span> [ <span class="string">&quot;<span class="variable">$&#123;HAS_JENKINSFILE&#125;</span>&quot;</span> = <span class="string">&quot;no&quot;</span> ] &amp;&amp; [ <span class="string">&quot;<span class="variable">$&#123;HAS_GHA&#125;</span>&quot;</span> = <span class="string">&quot;yes&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;[GHA only]        <span class="variable">$&#123;repo&#125;</span>&quot;</span></span><br><span class="line">      <span class="keyword">elif</span> [ <span class="string">&quot;<span class="variable">$&#123;HAS_JENKINSFILE&#125;</span>&quot;</span> = <span class="string">&quot;yes&quot;</span> ] &amp;&amp; [ <span class="string">&quot;<span class="variable">$&#123;HAS_GHA&#125;</span>&quot;</span> = <span class="string">&quot;yes&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;[Parallel run]    <span class="variable">$&#123;repo&#125;</span>&quot;</span></span><br><span class="line">      <span class="keyword">elif</span> [ <span class="string">&quot;<span class="variable">$&#123;HAS_CONFIG&#125;</span>&quot;</span> = <span class="string">&quot;yes&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;[No CI!]          <span class="variable">$&#123;repo&#125;</span>&quot;</span></span><br><span class="line">      <span class="keyword">fi</span></span><br><span class="line">    <span class="keyword">done</span> | <span class="built_in">sort</span> | <span class="built_in">tee</span> migration_status.txt</span><br><span class="line"></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;&quot;</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;统计：&quot;</span></span><br><span class="line">grep -c <span class="string">&quot;\[Jenkins only\]&quot;</span> migration_status.txt | xargs <span class="built_in">echo</span> <span class="string">&quot;  Jenkins only:&quot;</span></span><br><span class="line">grep -c <span class="string">&quot;\[GHA only\]&quot;</span> migration_status.txt     | xargs <span class="built_in">echo</span> <span class="string">&quot;  GitHub Actions only:&quot;</span></span><br><span class="line">grep -c <span class="string">&quot;\[Parallel run\]&quot;</span> migration_status.txt | xargs <span class="built_in">echo</span> <span class="string">&quot;  并行运行中:&quot;</span></span><br><span class="line">grep -c <span class="string">&quot;\[No CI!\]&quot;</span> migration_status.txt       | xargs <span class="built_in">echo</span> <span class="string">&quot;  无 CI（需要接入）:&quot;</span></span><br></pre></td></tr></table></figure><p>这个脚本在 500 个仓库的迁移过程中，是平台团队每周例会的标准输入。</p><h3 id="策略六：保留回滚方案（30-天窗口）"><a href="#策略六：保留回滚方案（30-天窗口）" class="headerlink" title="策略六：保留回滚方案（30 天窗口）"></a>策略六：保留回滚方案（30 天窗口）</h3><p>在切换后的 30 天内，保留 Jenkins Job 但暂停触发：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 旧 Jenkinsfile 重命名为 .jenkinsfile.bak 保留在仓库中</span></span><br><span class="line"><span class="comment">// 如果 GitHub Actions 出现严重问题，可以临时恢复文件名并在 Jenkins 手动触发</span></span><br></pre></td></tr></table></figure><p><strong>500+ 仓库的回滚策略：</strong> 不要在整个 500 个仓库上同时删除 Jenkins 配置。按批次删除，每批次等待 30 天观察期。一旦出现平台级问题（比如 runner 容量不足导致排队严重），可以将该批次已迁移的仓库先回滚，同时扩容 runner，再重新迁移。</p><hr><h2 id="六、决策树（500-仓库版）"><a href="#六、决策树（500-仓库版）" class="headerlink" title="六、决策树（500+ 仓库版）"></a>六、决策树（500+ 仓库版）</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">Q1: 你的仓库数量是否超过 50 个？</span><br><span class="line">│</span><br><span class="line">├─ 否 → 统一管理的收益可能低于成本，参考小规模建议</span><br><span class="line">│</span><br><span class="line">└─ 是 → Q2: Jenkins 是否运行稳定（过去一年全量 CI 中断 ≤ 2 次）？</span><br><span class="line">          │</span><br><span class="line">          ├─ 是 → Q3: 是否有明确的凭证安全合规要求（SOC 2 / ISO 27001）？</span><br><span class="line">          │         │</span><br><span class="line">          │         ├─ 否 → Q4: 工程师排查 CI 失败是否需要频繁切换平台，</span><br><span class="line">          │         │            且团队规模 &gt; 50 人？</span><br><span class="line">          │         │         │</span><br><span class="line">          │         │         ├─ 否 → 继续使用 Jenkins，每年评估一次</span><br><span class="line">          │         │         └─ 是 → 计算年化节省时间（见第三节驱动力四）</span><br><span class="line">          │         │                  若 &gt; 500 小时/年，评估迁移 ROI</span><br><span class="line">          │         │</span><br><span class="line">          │         └─ 是 → Q5: 超过 10% 的仓库依赖动态 Stage 生成？</span><br><span class="line">          │                   │</span><br><span class="line">          │                   ├─ 是 → 维持 Jenkins，加固 AppRole 轮换流程</span><br><span class="line">          │                   │        （动态 stage 迁移成本过高）</span><br><span class="line">          │                   └─ 否 → 推荐迁移，优先从 Dev/Stg 环境 Org 开始</span><br><span class="line">          │</span><br><span class="line">          └─ 否（Jenkins 不稳定）→ Q6: 不稳定原因是否是规模性问题？</span><br><span class="line">                    │              （OOM、Vault 限流、插件兼容）</span><br><span class="line">                    │</span><br><span class="line">                    ├─ 否（随机故障）→ 先解决根本原因，再评估迁移</span><br><span class="line">                    │</span><br><span class="line">                    └─ 是（规模性问题）→ Q7: 超过 10% 的仓库依赖动态 Stage？</span><br><span class="line">                                          │</span><br><span class="line">                                          ├─ 是 → 迁移部分仓库（无自定义 stage），</span><br><span class="line">                                          │        保留复杂仓库在 Jenkins</span><br><span class="line">                                          └─ 否 → 强烈推荐迁移</span><br><span class="line">                                                   Jenkins 维护成本已超过迁移成本</span><br></pre></td></tr></table></figure><hr><h2 id="七、两种方案能否共存？（500-仓库的现实选择）"><a href="#七、两种方案能否共存？（500-仓库的现实选择）" class="headerlink" title="七、两种方案能否共存？（500+ 仓库的现实选择）"></a>七、两种方案能否共存？（500+ 仓库的现实选择）</h2><p><strong>可以，且在 500+ 仓库规模下，这往往是比”完全迁移”更现实的长期策略。</strong></p><h3 id="永久共存的合理场景"><a href="#永久共存的合理场景" class="headerlink" title="永久共存的合理场景"></a>永久共存的合理场景</h3><ul><li><strong>动态 Stage 仓库永留 Jenkins</strong>：约 5-10% 的仓库因为大量自定义 stage，迁移成本远超收益，接受永久双系统</li><li><strong>新项目全走 GitHub Actions，存量仓库按节奏迁移</strong>：不设硬性截止日期，随仓库的正常维护周期自然迁移</li><li><strong>不同 CI 复杂度分流</strong>：Jenkins 处理复杂的多环境部署流水线，GitHub Actions 处理标准构建 + 测试</li></ul><h3 id="ci-config-config-yaml-的战略价值"><a href="#ci-config-config-yaml-的战略价值" class="headerlink" title=".ci-config/config.yaml 的战略价值"></a><code>.ci-config/config.yaml</code> 的战略价值</h3><p>这个接口文件是两套系统可以共存的关键：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># .ci-config/config.yaml 的结构对两套系统都有效</span></span><br><span class="line"><span class="attr">containers:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">project-runtime</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">platform-registry.example.com/python:3.12</span></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">lint</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">pyLint:</span></span><br><span class="line">          <span class="attr">sourceSets:</span> [<span class="string">src/</span>]</span><br></pre></td></tr></table></figure><ul><li>Jenkins 的 <code>ConfigMerger.groovy</code> 读取它</li><li>GitHub Actions 的 <code>config</code> job 的 shell 脚本读取它</li></ul><p><strong>业务团队不需要知道底层是哪套系统</strong>——这是接口设计的价值所在。当平台团队决定在某个仓库上切换底层 CI 引擎时，业务团队唯一的感知是”CI 结果显示的位置变了”。</p><h3 id="长期策略建议（更新的规模阈值）"><a href="#长期策略建议（更新的规模阈值）" class="headerlink" title="长期策略建议（更新的规模阈值）"></a>长期策略建议（更新的规模阈值）</h3><table><thead><tr><th>规模</th><th>建议策略</th></tr></thead><tbody><tr><td>&lt; 20 个仓库</td><td>维持现状，不引入新复杂度</td></tr><tr><td>20-100 个仓库</td><td>新仓库 GitHub Actions，存量仓库按需迁移</td></tr><tr><td>100-500 个仓库</td><td>制定正式波次迁移计划，保留混合架构过渡期（1-2 年）</td></tr><tr><td>&gt; 500 个仓库</td><td>混合架构可能是永久状态；以接口统一（<code>.ci-config/config.yaml</code>）代替技术统一</td></tr></tbody></table><p>在 500+ 仓库规模下，<strong>追求 100% 迁移不是目标</strong>——目标是让业务团队有统一的 CI 接入体验，同时让平台团队的维护工作收敛。即使 10% 的仓库永远留在 Jenkins，只要接口统一，这 10% 的维护成本是可接受的。</p><h3 id="可观测性：双系统监控"><a href="#可观测性：双系统监控" class="headerlink" title="可观测性：双系统监控"></a>可观测性：双系统监控</h3><p>在混合架构运行期间，平台团队需要统一的 CI 健康视图，跨越两套系统：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">需要回答的问题：</span><br><span class="line">  - 今天 Jenkins CI 失败了多少次？GitHub Actions 失败了多少次？</span><br><span class="line">  - 两套系统的平均耗时对比？</span><br><span class="line">  - 哪些仓库的 Jenkins CI 超过 30 天没有运行（可以加速迁移）？</span><br><span class="line">  - 当前双轨运行的仓库数量（并行验证进度）？</span><br></pre></td></tr></table></figure><p>这需要 Jenkins 的 build 数据和 GitHub Actions 的 workflow 数据汇聚到同一个 dashboard（如 Grafana），以避免平台团队在两个系统之间手动关联信息。</p><hr><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>本系列四篇文章从”为什么”出发，经过 Jenkins 和 GitHub Actions 的技术实现，到今天的迁移决策框架，试图回答一个完整的问题：<strong>如何在 500+ 仓库的工程组织中治理 CI&#x2F;CD 流水线，以及在不同阶段应该选择什么工具。</strong></p><p>核心结论：</p><ol><li><strong>统一管理的价值是真实的</strong>——分离关注点让平台团队和业务团队各自专注，这个价值在 500+ 仓库时乘以 500 倍的规模效应</li><li><strong>Jenkins Shared Library 是成熟可靠的方案</strong>，不是”旧技术”；在 500+ 规模下它的挑战是运维复杂度，而非功能缺失</li><li><strong>GitHub Actions 的 JWT&#x2F;OIDC + Reusable Workflow</strong> 在凭证安全和开发者体验上有实质性优势，且在 500+ 规模下这些优势被进一步放大</li><li><strong>迁移的决策应该基于 ROI，而不是技术时髦性</strong>——500 个仓库的迁移是一个以年为单位的工程项目</li><li><strong><code>.ci-config/config.yaml</code> 这样的接口设计让平台迁移对业务团队透明</strong>——这是 500+ 仓库规模下迁移可行性的关键设计决策</li><li><strong>混合架构可能是 500+ 仓库规模下的长期现实</strong>——以接口统一代替技术统一，是更务实的目标</li></ol><p>无论你今天的选择是什么，最重要的原则是：<strong>让业务团队只需要声明意图，不需要理解实现</strong>。这个原则在 Jenkins 和 GitHub Actions 中都适用，在 5 个仓库和 500 个仓库时都成立。</p>]]>
    </content>
    <id>https://www.realks.com/2026/06/21/unified-cicd-04-migration/</id>
    <link href="https://www.realks.com/2026/06/21/unified-cicd-04-migration/"/>
    <published>2026-06-21T02:30:00.000Z</published>
    <summary>
      <![CDATA[<h1 id="从-Jenkins-迁移到-GitHub-Actions：决策框架与方案对比"><a href="#从-Jenkins-迁移到-GitHub-Actions：决策框架与方案对比" class="headerlink" title="从 Jenkins 迁移到 GitHub Actions：决策框架与方案对比"></a>从 Jenkins 迁移到 GitHub Actions：决策框架与方案对比</h1><blockquote>
<p>本文是《统一 CI&#x2F;CD 流水线治理》系列第四篇，也是最后一篇。前三篇分别介绍了为什么要统一管理、Jenkins 如何实现、GitHub Actions 如何实现。本文的目的是帮助你做出有依据的决策：<strong>要不要迁移，以及如何迁移</strong>。本文来自一个覆盖 500+ 仓库、运行 2 年以上的生产实践——迁移决策不是纸上谈兵，而是真实发生过的权衡。</p>
</blockquote>
<hr>]]>
    </summary>
    <title>从 Jenkins 迁移到 GitHub Actions：决策框架与方案对比</title>
    <updated>2026-06-21T02:30:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="DevOps" scheme="https://www.realks.com/categories/DevOps/"/>
    <category term="CI/CD" scheme="https://www.realks.com/categories/DevOps/CI-CD/"/>
    <category term="CI/CD" scheme="https://www.realks.com/tags/CI-CD/"/>
    <category term="Platform Engineering" scheme="https://www.realks.com/tags/Platform-Engineering/"/>
    <category term="GitHub Actions" scheme="https://www.realks.com/tags/GitHub-Actions/"/>
    <category term="Reusable Workflow" scheme="https://www.realks.com/tags/Reusable-Workflow/"/>
    <category term="JWT" scheme="https://www.realks.com/tags/JWT/"/>
    <category term="OIDC" scheme="https://www.realks.com/tags/OIDC/"/>
    <content>
      <![CDATA[<h1 id="GitHub-Actions-Reusable-Workflow：零配置统一-CI-CD-的完整实现"><a href="#GitHub-Actions-Reusable-Workflow：零配置统一-CI-CD-的完整实现" class="headerlink" title="GitHub Actions Reusable Workflow：零配置统一 CI&#x2F;CD 的完整实现"></a>GitHub Actions Reusable Workflow：零配置统一 CI&#x2F;CD 的完整实现</h1><blockquote><p>本文是《统一 CI&#x2F;CD 流水线治理》系列第三篇。本文深入拆解平台团队如何用 Reusable Workflow 实现”业务仓库零配置接入”的完整方案，涵盖架构设计、JWT&#x2F;OIDC 认证、多环境路由、容器构建以及踩坑记录。本文来自一个覆盖 500+ 仓库、生产运行中的实践。</p></blockquote><hr><span id="more"></span><h2 id="第一部分：架构概述"><a href="#第一部分：架构概述" class="headerlink" title="第一部分：架构概述"></a>第一部分：架构概述</h2><h3 id="github-仓库作为平台边界"><a href="#github-仓库作为平台边界" class="headerlink" title=".github 仓库作为平台边界"></a><code>.github</code> 仓库作为平台边界</h3><p>在 GitHub Organization 中，名为 <code>.github</code> 的特殊仓库承担两类职责：一是存放 Organization 级别的默认 Community Health Files（<code>CODE_OF_CONDUCT.md</code> 等），二是存放 Reusable Workflow 文件供 Org 内所有仓库调用。</p><p>平台团队将所有 CI&#x2F;CD 逻辑集中在 <code>.github</code> 仓库，形成清晰的平台边界：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">OrgA/.github</span><br><span class="line">├── .github/</span><br><span class="line">│   └── workflows/</span><br><span class="line">│       ├── platform-ci-core.yml          # 编排器 / 入口</span><br><span class="line">│       ├── platform-ci-check.yml         # lint + 单元测试</span><br><span class="line">│       ├── platform-ci-security.yml      # 静态代码扫描</span><br><span class="line">│       ├── platform-ci-build.yml         # 容器构建 + 多仓库推送 + 签名</span><br><span class="line">│       ├── platform-ci-prepare-release.yml</span><br><span class="line">│       ├── platform-ci-release.yml</span><br><span class="line">│       └── platform-ci-deploy.yml</span><br><span class="line">└── actions/</span><br><span class="line">    ├── prepare-release/</span><br><span class="line">    ├── release/</span><br><span class="line">    └── security-scan/</span><br></pre></td></tr></table></figure><p>500+ 个业务仓库全部调用这同一套 workflow 文件。每个业务仓库只需维护一个极简的 <code>ci.yml</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># .github/workflows/ci.yml（业务仓库，约 15 行）</span></span><br><span class="line"><span class="attr">name:</span> <span class="string">CI</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">trunk</span>, <span class="string">releases/latest</span>]</span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">trunk</span>, <span class="string">releases/latest</span>]</span><br><span class="line">  <span class="attr">pull_request_target:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">trunk</span>, <span class="string">releases/latest</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">pipeline:</span></span><br><span class="line">    <span class="comment"># fork PR 用 pull_request_target，同 repo PR 用 pull_request</span></span><br><span class="line">    <span class="attr">if:</span> <span class="string">&gt;-</span></span><br><span class="line"><span class="string">      github.event_name != &#x27;pull_request_target&#x27; ||</span></span><br><span class="line"><span class="string">      github.event.pull_request.head.repo.full_name != github.repository</span></span><br><span class="line"><span class="string"></span>    <span class="attr">uses:</span> <span class="string">OrgA/.github/.github/workflows/platform-ci-core.yml@main</span></span><br><span class="line">    <span class="comment"># 无 with: 块 —— 所有配置从 .ci-config/config.yaml 读取</span></span><br><span class="line">    <span class="comment"># 无 secrets: 块 —— 凭证由平台流水线内部处理</span></span><br></pre></td></tr></table></figure><h4 id="pull-request-target-的必要性"><a href="#pull-request-target-的必要性" class="headerlink" title="pull_request_target 的必要性"></a><code>pull_request_target</code> 的必要性</h4><p><code>pull_request</code> 事件在 fork PR 场景下无法访问 Org Secrets，导致需要 Vault 凭证的 job 全部失败。改用 <code>pull_request_target</code> 后，workflow 在 <strong>base 仓库的上下文</strong>中运行，可以访问 Secrets。但这带来了安全隐患——详见踩坑经验章节。</p><hr><h3 id="Workflow-文件职责说明"><a href="#Workflow-文件职责说明" class="headerlink" title="Workflow 文件职责说明"></a>Workflow 文件职责说明</h3><table><thead><tr><th>文件</th><th>职责</th></tr></thead><tbody><tr><td><code>platform-ci-core.yml</code></td><td>编排器，包含 <code>config</code> job，调用所有下游 reusable workflow</td></tr><tr><td><code>platform-ci-check.yml</code></td><td>pylint、shellcheck、单元测试</td></tr><tr><td><code>platform-ci-security.yml</code></td><td>SonarQube &#x2F; Semgrep 扫描</td></tr><tr><td><code>platform-ci-build.yml</code></td><td>buildx 构建、多仓库推送、镜像签名</td></tr><tr><td><code>platform-ci-prepare-release.yml</code></td><td>计算下一个语义化版本号</td></tr><tr><td><code>platform-ci-release.yml</code></td><td>打 Git tag、创建 GitHub Release</td></tr><tr><td><code>platform-ci-deploy.yml</code></td><td>状态上报、Docker 信息推送、健康检查</td></tr></tbody></table><hr><h2 id="第二部分：config-job-—-替代-Groovy-Merger-的关键设计"><a href="#第二部分：config-job-—-替代-Groovy-Merger-的关键设计" class="headerlink" title="第二部分：config job — 替代 Groovy Merger 的关键设计"></a>第二部分：config job — 替代 Groovy Merger 的关键设计</h2><h3 id="为什么需要专门的-config-job"><a href="#为什么需要专门的-config-job" class="headerlink" title="为什么需要专门的 config job"></a>为什么需要专门的 config job</h3><p>GitHub Actions 的 workflow 结构是<strong>静态</strong>的：<code>with:</code> 字段的值在 workflow 解析时就必须确定类型，<code>if:</code> 条件在 job 级别也有语法限制。平台无法像 Jenkins Groovy 那样在运行时动态合并配置。</p><p>解决方案是在编排器中设置一个 <code>config</code> job，专门读取业务仓库的 <code>.ci-config/config.yaml</code>，将所有派生配置作为 <code>outputs</code> 输出，供下游 job 通过 <code>needs.config.outputs.*</code> 消费。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">.ci-config/config.yaml（业务仓库声明）</span><br><span class="line">        │</span><br><span class="line">        ▼</span><br><span class="line">   config job（解析 + 计算，一次运行）</span><br><span class="line">        │</span><br><span class="line">        ├──► platform-ci-check.yml   (python_version, pylint_sources)</span><br><span class="line">        ├──► platform-ci-security.yml (sonar_project_key)</span><br><span class="line">        ├──► platform-ci-build.yml   (dockerfile, image_url_list)</span><br><span class="line">        └──► platform-ci-deploy.yml  (content_name, image_url_list)</span><br></pre></td></tr></table></figure><h3 id="config-job-的完整-shell-实现"><a href="#config-job-的完整-shell-实现" class="headerlink" title="config job 的完整 shell 实现"></a><code>config</code> job 的完整 shell 实现</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># platform-ci-core.yml（config job 节选）</span></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">config:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">Resolve</span> <span class="string">Config</span></span><br><span class="line">    <span class="attr">runs-on:</span> [<span class="string">self-hosted</span>, <span class="string">linux</span>]</span><br><span class="line">    <span class="attr">outputs:</span></span><br><span class="line">      <span class="attr">python_version:</span>       <span class="string">$&#123;&#123;</span> <span class="string">steps.resolve.outputs.python_version</span> <span class="string">&#125;&#125;</span></span><br><span class="line">      <span class="attr">pylint_module_paths:</span>  <span class="string">$&#123;&#123;</span> <span class="string">steps.resolve.outputs.pylint_module_paths</span> <span class="string">&#125;&#125;</span></span><br><span class="line">      <span class="attr">pylint_rc_file:</span>       <span class="string">$&#123;&#123;</span> <span class="string">steps.resolve.outputs.pylint_rc_file</span> <span class="string">&#125;&#125;</span></span><br><span class="line">      <span class="attr">has_unit_test:</span>        <span class="string">$&#123;&#123;</span> <span class="string">steps.resolve.outputs.has_unit_test</span> <span class="string">&#125;&#125;</span></span><br><span class="line">      <span class="attr">unit_test_script:</span>     <span class="string">$&#123;&#123;</span> <span class="string">steps.resolve.outputs.unit_test_script</span> <span class="string">&#125;&#125;</span></span><br><span class="line">      <span class="attr">dockerfile:</span>           <span class="string">$&#123;&#123;</span> <span class="string">steps.resolve.outputs.dockerfile</span> <span class="string">&#125;&#125;</span></span><br><span class="line">      <span class="attr">image_url_list:</span>       <span class="string">$&#123;&#123;</span> <span class="string">steps.resolve.outputs.image_url_list</span> <span class="string">&#125;&#125;</span></span><br><span class="line">      <span class="attr">sonar_project_key:</span>    <span class="string">$&#123;&#123;</span> <span class="string">steps.resolve.outputs.sonar_project_key</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">ref:</span> <span class="string">$&#123;&#123;</span> <span class="string">github.event.pull_request.head.sha</span> <span class="string">||</span> <span class="string">github.sha</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">yq</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          if ! command -v yq &amp;&gt;/dev/null; then</span></span><br><span class="line"><span class="string">            curl -fsSL https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \</span></span><br><span class="line"><span class="string">              -o /usr/local/bin/yq &amp;&amp; chmod +x /usr/local/bin/yq</span></span><br><span class="line"><span class="string">          fi</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Parse</span> <span class="string">.ci-config/config.yaml</span></span><br><span class="line">        <span class="attr">id:</span> <span class="string">resolve</span></span><br><span class="line">        <span class="attr">env:</span></span><br><span class="line">          <span class="attr">REPO_NAME:</span>  <span class="string">$&#123;&#123;</span> <span class="string">github.event.repository.name</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">REPO_OWNER:</span> <span class="string">$&#123;&#123;</span> <span class="string">github.repository_owner</span> <span class="string">&#125;&#125;</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          CFG=&quot;.ci-config/config.yaml&quot;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">          <span class="comment"># Python 版本：从镜像 tag 中提取语义化版本</span></span><br><span class="line">          <span class="comment"># 例：platform-registry.example.com/python:3.14 → 3.14</span></span><br><span class="line">          <span class="string">RAW_IMAGE=$(yq</span> <span class="string">&#x27;.containers[] | select(.name==&quot;project-runtime&quot;) | .image&#x27;</span> <span class="string">\</span></span><br><span class="line">            <span class="string">&quot;$&#123;CFG&#125;&quot;</span> <span class="number">2</span><span class="string">&gt;/dev/null</span> <span class="string">||</span> <span class="string">echo</span> <span class="string">&quot;&quot;</span><span class="string">)</span></span><br><span class="line">          <span class="string">PYTHON_VERSION=$(echo</span> <span class="string">&quot;$&#123;RAW_IMAGE&#125;&quot;</span> <span class="string">|</span> <span class="string">\</span></span><br><span class="line">            <span class="string">grep</span> <span class="string">-oE</span> <span class="string">&#x27;[0-9]+\.[0-9]+(\.[0-9]+)?$&#x27;</span> <span class="string">||</span> <span class="string">echo</span> <span class="string">&quot;3.x&quot;</span><span class="string">)</span></span><br><span class="line">          [ <span class="string">-z</span> <span class="string">&quot;$&#123;PYTHON_VERSION&#125;&quot;</span> ] <span class="string">&amp;&amp;</span> <span class="string">PYTHON_VERSION=&quot;3.x&quot;</span></span><br><span class="line"></span><br><span class="line">          <span class="comment"># pylint sourceSets → 空格分隔字符串</span></span><br><span class="line">          <span class="string">PYLINT_PATHS=$(yq</span> <span class="string">&#x27;.jobs[] | select(.name==&quot;lint&quot;) | .steps[].pyLint.sourceSets[]&#x27;</span> <span class="string">\</span></span><br><span class="line">            <span class="string">&quot;$&#123;CFG&#125;&quot;</span> <span class="number">2</span><span class="string">&gt;/dev/null</span> <span class="string">|</span> <span class="string">tr</span> <span class="string">&#x27;\n&#x27;</span> <span class="string">&#x27; &#x27;</span> <span class="string">|</span> <span class="string">xargs</span> <span class="string">||</span> <span class="string">echo</span> <span class="string">&quot;&quot;</span><span class="string">)</span></span><br><span class="line"></span><br><span class="line">          <span class="comment"># pylint rcFile：过滤 yq 的 null 输出</span></span><br><span class="line">          <span class="string">PYLINT_RC=$(yq</span> <span class="string">&#x27;.jobs[] | select(.name==&quot;lint&quot;) | .steps[].pyLint.rcFile&#x27;</span> <span class="string">\</span></span><br><span class="line">            <span class="string">&quot;$&#123;CFG&#125;&quot;</span> <span class="number">2</span><span class="string">&gt;/dev/null</span> <span class="string">|</span> <span class="string">grep</span> <span class="string">-v</span> <span class="string">&#x27;^null$&#x27;</span> <span class="string">||</span> <span class="string">echo</span> <span class="string">&quot;&quot;</span><span class="string">)</span></span><br><span class="line"></span><br><span class="line">          <span class="comment"># 单元测试检测</span></span><br><span class="line">          <span class="string">UNIT_TEST_SCRIPT=$(yq</span> <span class="string">&#x27;.jobs[] | select(.name==&quot;unit-test&quot;) | .steps[].script.workspace&#x27;</span> <span class="string">\</span></span><br><span class="line">            <span class="string">&quot;$&#123;CFG&#125;&quot;</span> <span class="number">2</span><span class="string">&gt;/dev/null</span> <span class="string">|</span> <span class="string">grep</span> <span class="string">-v</span> <span class="string">&#x27;^null$&#x27;</span> <span class="string">|</span> <span class="string">head</span> <span class="number">-1</span> <span class="string">||</span> <span class="string">echo</span> <span class="string">&quot;&quot;</span><span class="string">)</span></span><br><span class="line">          <span class="string">HAS_UNIT_TEST=&quot;false&quot;</span></span><br><span class="line">          [ <span class="string">-n</span> <span class="string">&quot;$&#123;UNIT_TEST_SCRIPT&#125;&quot;</span> ] <span class="string">&amp;&amp;</span> <span class="string">HAS_UNIT_TEST=&quot;true&quot;</span></span><br><span class="line"></span><br><span class="line">          <span class="comment"># containerBuild 配置</span></span><br><span class="line">          <span class="string">DOCKERFILE=$(yq</span> <span class="string">&#x27;.containerBuild.path&#x27;</span> <span class="string">&quot;$&#123;CFG&#125;&quot;</span> <span class="number">2</span><span class="string">&gt;/dev/null</span> <span class="string">|</span> <span class="string">grep</span> <span class="string">-v</span> <span class="string">&#x27;^null$&#x27;</span> <span class="string">||</span> <span class="string">echo</span> <span class="string">&quot;&quot;</span><span class="string">)</span></span><br><span class="line">          <span class="string">IMAGE_TYPE=$(yq</span> <span class="string">&#x27;.containerBuild.registryType&#x27;</span> <span class="string">&quot;$&#123;CFG&#125;&quot;</span> <span class="number">2</span><span class="string">&gt;/dev/null</span> <span class="string">|</span> <span class="string">\</span></span><br><span class="line">            <span class="string">grep</span> <span class="string">-v</span> <span class="string">&#x27;^null$&#x27;</span> <span class="string">||</span> <span class="string">echo</span> <span class="string">&quot;internet&quot;</span><span class="string">)</span></span><br><span class="line"></span><br><span class="line">          <span class="comment"># 根据 image_type 计算目标 registry</span></span><br><span class="line">          <span class="string">case</span> <span class="string">&quot;$&#123;IMAGE_TYPE&#125;&quot;</span> <span class="string">in</span></span><br><span class="line">            <span class="string">private)</span>  <span class="string">PRIMARY_REG=&quot;internal-private.platform-registry.example.com&quot;</span> <span class="string">;;</span></span><br><span class="line">            <span class="string">public)</span>   <span class="string">PRIMARY_REG=&quot;internal-public.platform-registry.example.com&quot;</span> <span class="string">;;</span></span><br><span class="line">            <span class="string">internet)</span> <span class="string">PRIMARY_REG=&quot;internet.platform-registry.example.com&quot;</span> <span class="string">;;</span></span><br><span class="line">            <span class="string">*)</span>        <span class="string">PRIMARY_REG=&quot;internal.platform-registry.example.com&quot;</span> <span class="string">;;</span></span><br><span class="line">          <span class="string">esac</span></span><br><span class="line">          <span class="string">PRIMARY_URL=&quot;$&#123;PRIMARY_REG&#125;/$&#123;REPO_OWNER&#125;/$&#123;REPO_NAME&#125;&quot;</span></span><br><span class="line"></span><br><span class="line">          <span class="comment"># internet 类型额外推送到公开仓库</span></span><br><span class="line">          <span class="string">if</span> [ <span class="string">&quot;$&#123;IMAGE_TYPE&#125;&quot;</span> <span class="string">=</span> <span class="string">&quot;internet&quot;</span> ]<span class="string">;</span> <span class="string">then</span></span><br><span class="line">            <span class="string">PUBLIC_URL=&quot;public.platform-registry.example.com/$&#123;REPO_OWNER&#125;/$&#123;REPO_NAME&#125;&quot;</span></span><br><span class="line">            <span class="string">IMAGE_URL_LIST=&quot;$&#123;PRIMARY_URL&#125;,$&#123;PUBLIC_URL&#125;&quot;</span></span><br><span class="line">          <span class="string">else</span></span><br><span class="line">            <span class="string">IMAGE_URL_LIST=&quot;$&#123;PRIMARY_URL&#125;&quot;</span></span><br><span class="line">          <span class="string">fi</span></span><br><span class="line">          <span class="string">IMAGE_URL_LIST=&quot;$&#123;IMAGE_URL_LIST,,&#125;&quot;</span>  <span class="comment"># 转小写</span></span><br><span class="line"></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;python_version=$&#123;PYTHON_VERSION&#125;&quot;</span>     <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_OUTPUT&quot;</span></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;pylint_module_paths=$&#123;PYLINT_PATHS&#125;&quot;</span>  <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_OUTPUT&quot;</span></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;pylint_rc_file=$&#123;PYLINT_RC&#125;&quot;</span>          <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_OUTPUT&quot;</span></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;has_unit_test=$&#123;HAS_UNIT_TEST&#125;&quot;</span>       <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_OUTPUT&quot;</span></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;unit_test_script=$&#123;UNIT_TEST_SCRIPT&#125;&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_OUTPUT&quot;</span></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;dockerfile=$&#123;DOCKERFILE&#125;&quot;</span>             <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_OUTPUT&quot;</span></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;image_url_list=$&#123;IMAGE_URL_LIST&#125;&quot;</span>     <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_OUTPUT&quot;</span></span><br><span class="line"></span><br><span class="line">          <span class="comment"># Job Summary：配置解析结果表格（调试友好，500+ 仓库的支持成本关键）</span></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;### Config resolved from .ci-config/config.yaml&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_STEP_SUMMARY&quot;</span></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;| Key | Value |&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_STEP_SUMMARY&quot;</span></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;|-----|-------|&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_STEP_SUMMARY&quot;</span></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;| python_version | \`$&#123;PYTHON_VERSION&#125;\` |&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_STEP_SUMMARY&quot;</span></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;| pylint_module_paths | \`$&#123;PYLINT_PATHS&#125;\` |&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_STEP_SUMMARY&quot;</span></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;| has_unit_test | \`$&#123;HAS_UNIT_TEST&#125;\` |&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_STEP_SUMMARY&quot;</span></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;| dockerfile | \`$&#123;DOCKERFILE:-&lt;none&gt;&#125;\` |&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_STEP_SUMMARY&quot;</span></span><br><span class="line">          <span class="string">echo</span> <span class="string">&quot;| image_url_list | \`$&#123;IMAGE_URL_LIST&#125;\` |&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_STEP_SUMMARY&quot;</span></span><br></pre></td></tr></table></figure><p><strong>Job Summary 的重要性在 500+ 规模下被放大</strong>：当业务团队反馈”CI 行为不符合预期”时，平台团队需要快速定位是 <code>config.yaml</code> 解析问题还是流水线逻辑问题。Step Summary 中的配置表格让这个诊断从”需要查日志”变成”打开 PR 页面就能看到”。</p><h3 id="下游调用方式"><a href="#下游调用方式" class="headerlink" title="下游调用方式"></a>下游调用方式</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">build:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">Build</span> <span class="string">Container</span> <span class="string">Image</span></span><br><span class="line">  <span class="attr">needs:</span> [<span class="string">config</span>, <span class="string">security</span>]</span><br><span class="line">  <span class="attr">if:</span> <span class="string">$&#123;&#123;</span> <span class="string">needs.config.outputs.dockerfile</span> <span class="type">!=</span> <span class="string">&#x27;&#x27;</span> <span class="string">&#125;&#125;</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">OrgA/.github/.github/workflows/platform-ci-build.yml@main</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">dockerfile:</span>     <span class="string">$&#123;&#123;</span> <span class="string">needs.config.outputs.dockerfile</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">image_url_list:</span> <span class="string">$&#123;&#123;</span> <span class="string">needs.config.outputs.image_url_list</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><hr><h2 id="第三部分：JWT-OIDC-凭证架构深度解析"><a href="#第三部分：JWT-OIDC-凭证架构深度解析" class="headerlink" title="第三部分：JWT&#x2F;OIDC 凭证架构深度解析"></a>第三部分：JWT&#x2F;OIDC 凭证架构深度解析</h2><h3 id="job-workflow-ref-claim-的本质"><a href="#job-workflow-ref-claim-的本质" class="headerlink" title="job_workflow_ref claim 的本质"></a><code>job_workflow_ref</code> claim 的本质</h3><p>GitHub Actions OIDC token 中有一个关键 claim：<code>job_workflow_ref</code>。它的值是<strong>被调用 workflow 文件的路径</strong>，而非调用方仓库名。</p><p>当业务仓库 <code>OrgA/my-app</code>（500 个仓库之一）调用 <code>platform-ci-build.yml</code> 时：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">job_workflow_ref = &quot;OrgA/.github/.github/workflows/platform-ci-build.yml@refs/heads/main&quot;</span><br><span class="line">repository       = &quot;OrgA/my-app&quot;   ← 这是业务仓库，不是 .github</span><br></pre></td></tr></table></figure><p>Vault 的 <code>bound_claims</code> 绑定 <code>job_workflow_ref</code>——无论哪个业务仓库触发，只要调用的是平台 workflow 文件，就能通过认证。500 个仓库，1 个 Vault role，0 个静态凭证。</p><h3 id="完整认证流程"><a href="#完整认证流程" class="headerlink" title="完整认证流程"></a>完整认证流程</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">业务仓库 ci.yml（500 个中的任意一个）</span><br><span class="line">      │  (uses: OrgA/.github/...platform-ci-build.yml@main)</span><br><span class="line">      ▼</span><br><span class="line">platform-ci-build.yml（job 级别）</span><br><span class="line">      │  permissions:</span><br><span class="line">      │    id-token: write   ← 必须在 sub-workflow job 级别声明</span><br><span class="line">      │</span><br><span class="line">      ├─1─► GHE OIDC Endpoint（https://ghe.example.com/_services/token）</span><br><span class="line">      │       └── 返回 JWT，含 job_workflow_ref claim</span><br><span class="line">      │</span><br><span class="line">      ├─2─► vault-action (method: jwt)</span><br><span class="line">      │       ├── POST /v1/auth/jwt/login</span><br><span class="line">      │       │     &#123; jwt: &lt;oidc_token&gt;, role: &quot;platform-ci&quot; &#125;</span><br><span class="line">      │       │</span><br><span class="line">      │       └── Vault 验证流程：</span><br><span class="line">      │             ├── 获取 OIDC Discovery Document（JWKS endpoint）</span><br><span class="line">      │             ├── 验证 JWT 签名</span><br><span class="line">      │             ├── 检查 bound_claims（job_workflow_ref glob 匹配）</span><br><span class="line">      │             ├── 检查 @refs/heads/main 后缀（branch lock）</span><br><span class="line">      │             └── 返回 batch token（TTL: 5min）</span><br><span class="line">      │</span><br><span class="line">      └─3─► 使用 batch token 读取 KV secrets</span><br><span class="line">              （Registry 凭证、代码签名证书等）</span><br></pre></td></tr></table></figure><h3 id="Vault-Role-的-JSON-配置"><a href="#Vault-Role-的-JSON-配置" class="headerlink" title="Vault Role 的 JSON 配置"></a>Vault Role 的 JSON 配置</h3><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;role_type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;jwt&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;bound_audiences&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;https://vault.example.com&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;bound_claims_type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;glob&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;bound_claims&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;job_workflow_ref&quot;</span><span class="punctuation">:</span> <span class="string">&quot;OrgA/.github/.github/workflows/*@refs/heads/main&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;user_claim&quot;</span><span class="punctuation">:</span> <span class="string">&quot;repository&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;claim_mappings&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;repository&quot;</span><span class="punctuation">:</span>       <span class="string">&quot;repository&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;ref&quot;</span><span class="punctuation">:</span>              <span class="string">&quot;ref&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;workflow&quot;</span><span class="punctuation">:</span>         <span class="string">&quot;workflow&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;job_workflow_ref&quot;</span><span class="punctuation">:</span> <span class="string">&quot;job_workflow_ref&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;policies&quot;</span><span class="punctuation">:</span>               <span class="punctuation">[</span><span class="string">&quot;platform-ci&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;ttl&quot;</span><span class="punctuation">:</span>                    <span class="string">&quot;5m&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;max_ttl&quot;</span><span class="punctuation">:</span>                <span class="string">&quot;10m&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;token_type&quot;</span><span class="punctuation">:</span>             <span class="string">&quot;batch&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;token_no_default_policy&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><strong>在 500+ 仓库规模下，这个设计的安全价值尤为显著：</strong></p><ul><li>500 个业务仓库，无一存储任何凭证</li><li>任何一个业务仓库被攻击，攻击者仍无法获取平台凭证（<code>job_workflow_ref</code> 不匹配）</li><li>平台 workflow 文件的每次修改都必须经过 main 分支的 code review，<code>@refs/heads/main</code> 在 Vault 层强制执行</li></ul><blockquote><p><strong>注意</strong>：Vault CLI 不支持通过 <code>key=value</code> 传递 map 类型参数（<code>bound_claims</code>、<code>claim_mappings</code>）。必须使用 JSON heredoc stdin 格式：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">vault write auth/jwt/role/platform-ci - &lt;&lt;<span class="string">&#x27;EOF&#x27;</span></span><br><span class="line">&#123; ...full JSON... &#125;</span><br><span class="line">EOF</span><br></pre></td></tr></table></figure></blockquote><h3 id="batch-token-的三个关键属性"><a href="#batch-token-的三个关键属性" class="headerlink" title="batch token 的三个关键属性"></a>batch token 的三个关键属性</h3><ol><li><strong>不可续期</strong>：<code>vault token renew</code> 对 batch token 无效，TTL 到期即失效</li><li><strong>不可查询</strong>：不出现在 <code>vault list auth/token/accessors</code> 中，500 个仓库每天产生的数千个 token 都不留可查询踪迹</li><li><strong>5分钟 TTL</strong>：足够完成一次 <code>vault-action</code> 调用，过期后即使泄漏也无法使用</li></ol><h3 id="GHE-与-github-com-的-OIDC-Issuer-差异"><a href="#GHE-与-github-com-的-OIDC-Issuer-差异" class="headerlink" title="GHE 与 github.com 的 OIDC Issuer 差异"></a>GHE 与 github.com 的 OIDC Issuer 差异</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">github.com:  https://token.actions.githubusercontent.com</span><br><span class="line">GHE:         https://your-ghe-hostname/_services/token</span><br></pre></td></tr></table></figure><p>Vault 的 <code>oidc_discovery_url</code> 必须指向正确的 issuer，否则 Vault 无法获取正确的 JWKS endpoint 来验证签名：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># GHE 环境</span></span><br><span class="line">vault write auth/jwt/config \</span><br><span class="line">  oidc_discovery_url=<span class="string">&quot;https://ghe.example.com/_services/token&quot;</span> \</span><br><span class="line">  bound_issuer=<span class="string">&quot;https://ghe.example.com/_services/token&quot;</span></span><br></pre></td></tr></table></figure><h3 id="permissions-id-token-write-必须在每个-sub-workflow-的-job-级别声明"><a href="#permissions-id-token-write-必须在每个-sub-workflow-的-job-级别声明" class="headerlink" title="permissions: id-token: write 必须在每个 sub-workflow 的 job 级别声明"></a><code>permissions: id-token: write</code> 必须在每个 sub-workflow 的 job 级别声明</h3><p>编排器 <code>platform-ci-core.yml</code> 中声明的 <code>permissions</code> <strong>不会</strong>自动传递给通过 <code>uses:</code> 调用的 reusable workflow。每个需要获取 OIDC token 的 job 都必须独立声明：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># platform-ci-build.yml</span></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">build:</span></span><br><span class="line">    <span class="attr">runs-on:</span> [<span class="string">self-hosted</span>, <span class="string">linux</span>]</span><br><span class="line">    <span class="attr">permissions:</span></span><br><span class="line">      <span class="attr">id-token:</span> <span class="string">write</span>    <span class="comment"># 必须在这里声明，不能依赖编排器</span></span><br><span class="line">      <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">hashicorp/vault-action@v3</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">url:</span> <span class="string">$&#123;&#123;</span> <span class="string">vars.VAULT_URL</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">namespace:</span> <span class="string">$&#123;&#123;</span> <span class="string">vars.VAULT_NAMESPACE</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">method:</span> <span class="string">jwt</span></span><br><span class="line">          <span class="attr">role:</span> <span class="string">$&#123;&#123;</span> <span class="string">vars.VAULT_ROLE</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">jwtGithubAudience:</span> <span class="string">$&#123;&#123;</span> <span class="string">vars.VAULT_AUDIENCE</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">secrets:</span> <span class="string">|</span></span><br><span class="line"><span class="string">            secret/data/platform/$&#123;&#123; vars.VAULT_ENV &#125;&#125;/registry username | REGISTRY_USER ;</span></span><br><span class="line"><span class="string">            secret/data/platform/$&#123;&#123; vars.VAULT_ENV &#125;&#125;/registry password | REGISTRY_PASS</span></span><br></pre></td></tr></table></figure><hr><h2 id="第四部分：多环境路由——Org-Variables-方案"><a href="#第四部分：多环境路由——Org-Variables-方案" class="headerlink" title="第四部分：多环境路由——Org Variables 方案"></a>第四部分：多环境路由——Org Variables 方案</h2><h3 id="设计动机"><a href="#设计动机" class="headerlink" title="设计动机"></a>设计动机</h3><p>传统方案是在 workflow 中写 <code>if/else</code> 环境判断，导致 workflow 文件与环境强耦合。在 500+ 仓库规模下，任何一次环境配置变更都需要修改平台 workflow 文件并重新测试。</p><p>平台团队采用 <strong>Organization Variable 注入</strong>的方案：每个 Org 在创建时由平台脚本一次性写入环境相关变量，workflow 代码完全不包含环境分支逻辑，对三套环境（dev&#x2F;stg&#x2F;prod）使用完全相同的代码。</p><h3 id="六个-Org-Variables"><a href="#六个-Org-Variables" class="headerlink" title="六个 Org Variables"></a>六个 Org Variables</h3><table><thead><tr><th>Variable</th><th>OrgA-Dev</th><th>OrgA-Stg</th><th>OrgA（prod）</th></tr></thead><tbody><tr><td><code>VAULT_URL</code></td><td><code>https://vault.example.com</code></td><td>← 同</td><td>← 同</td></tr><tr><td><code>VAULT_NAMESPACE</code></td><td><code>platform/dev</code></td><td><code>platform/stg</code></td><td><code>platform/prod</code></td></tr><tr><td><code>VAULT_ROLE</code></td><td><code>platform-ci</code></td><td>← 同</td><td>← 同</td></tr><tr><td><code>VAULT_AUDIENCE</code></td><td><code>https://vault.example.com</code></td><td>← 同</td><td>← 同</td></tr><tr><td><code>VAULT_ENV</code></td><td><code>dev</code></td><td><code>stg</code></td><td><code>prod</code></td></tr><tr><td><code>API_URL</code></td><td><code>https://api.platform-dev.example.com</code></td><td><code>https://api.platform-stg.example.com</code></td><td><code>https://api.platform.example.com</code></td></tr></tbody></table><p>在 <code>vault-action</code> 的 <code>secrets:</code> 字段中，<code>$&#123;&#123; vars.VAULT_ENV &#125;&#125;</code> 自动插值：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">secrets:</span> <span class="string">|</span></span><br><span class="line">  <span class="string">secret/data/platform/$&#123;&#123;</span> <span class="string">vars.VAULT_ENV</span> <span class="string">&#125;&#125;/registry</span> <span class="string">username</span> <span class="string">|</span> <span class="string">REGISTRY_USER</span></span><br></pre></td></tr></table></figure><p><code>OrgA-Dev</code> 中 → <code>secret/data/platform/dev/registry</code><br><code>OrgA</code> 中 → <code>secret/data/platform/prod/registry</code></p><p><strong>workflow 代码一行不改，三个环境（覆盖 500+ 仓库）自动路由。</strong></p><h3 id="apply-org-variables-sh-幂等实现"><a href="#apply-org-variables-sh-幂等实现" class="headerlink" title="apply-org-variables.sh 幂等实现"></a><code>apply-org-variables.sh</code> 幂等实现</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/usr/bin/env bash</span></span><br><span class="line"><span class="comment"># 幂等写入 Organization Variables</span></span><br><span class="line"><span class="comment"># 已存在 → PATCH（更新），不存在 → POST（创建）</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">set</span> -euo pipefail</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="title">apply_org_vars</span></span>() &#123;</span><br><span class="line">  <span class="built_in">local</span> ORG=<span class="variable">$1</span></span><br><span class="line">  <span class="built_in">local</span> ENV=<span class="variable">$2</span></span><br><span class="line"></span><br><span class="line">  <span class="built_in">declare</span> -A VARS=(</span><br><span class="line">    [<span class="string">&quot;VAULT_URL&quot;</span>]=<span class="string">&quot;https://vault.example.com&quot;</span></span><br><span class="line">    [<span class="string">&quot;VAULT_NAMESPACE&quot;</span>]=<span class="string">&quot;platform/<span class="variable">$&#123;ENV&#125;</span>&quot;</span></span><br><span class="line">    [<span class="string">&quot;VAULT_ROLE&quot;</span>]=<span class="string">&quot;platform-ci&quot;</span></span><br><span class="line">    [<span class="string">&quot;VAULT_AUDIENCE&quot;</span>]=<span class="string">&quot;https://vault.example.com&quot;</span></span><br><span class="line">    [<span class="string">&quot;VAULT_ENV&quot;</span>]=<span class="string">&quot;<span class="variable">$&#123;ENV&#125;</span>&quot;</span></span><br><span class="line">    [<span class="string">&quot;API_URL&quot;</span>]=<span class="string">&quot;https://api.platform-<span class="variable">$&#123;ENV&#125;</span>.example.com&quot;</span></span><br><span class="line">  )</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> KEY <span class="keyword">in</span> <span class="string">&quot;<span class="variable">$&#123;!VARS[@]&#125;</span>&quot;</span>; <span class="keyword">do</span></span><br><span class="line">    VALUE=<span class="string">&quot;<span class="variable">$&#123;VARS[$KEY]&#125;</span>&quot;</span></span><br><span class="line">    HTTP_STATUS=$(gh api <span class="string">&quot;orgs/<span class="variable">$&#123;ORG&#125;</span>/actions/variables/<span class="variable">$&#123;KEY&#125;</span>&quot;</span> \</span><br><span class="line">      -i --silent 2&gt;&amp;1 | <span class="built_in">head</span> -1 | awk <span class="string">&#x27;&#123;print $2&#125;&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> [ <span class="string">&quot;<span class="variable">$&#123;HTTP_STATUS&#125;</span>&quot;</span> = <span class="string">&quot;200&quot;</span> ]; <span class="keyword">then</span></span><br><span class="line">      gh api --method PATCH <span class="string">&quot;orgs/<span class="variable">$&#123;ORG&#125;</span>/actions/variables/<span class="variable">$&#123;KEY&#125;</span>&quot;</span> \</span><br><span class="line">        -f value=<span class="string">&quot;<span class="variable">$&#123;VALUE&#125;</span>&quot;</span> -f visibility=<span class="string">&quot;all&quot;</span> --silent</span><br><span class="line">      <span class="built_in">echo</span> <span class="string">&quot;[UPDATE] <span class="variable">$&#123;ORG&#125;</span> / <span class="variable">$&#123;KEY&#125;</span>=<span class="variable">$&#123;VALUE&#125;</span>&quot;</span></span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">      gh api --method POST <span class="string">&quot;orgs/<span class="variable">$&#123;ORG&#125;</span>/actions/variables&quot;</span> \</span><br><span class="line">        -f name=<span class="string">&quot;<span class="variable">$&#123;KEY&#125;</span>&quot;</span> -f value=<span class="string">&quot;<span class="variable">$&#123;VALUE&#125;</span>&quot;</span> -f visibility=<span class="string">&quot;all&quot;</span> --silent</span><br><span class="line">      <span class="built_in">echo</span> <span class="string">&quot;[CREATE] <span class="variable">$&#123;ORG&#125;</span> / <span class="variable">$&#123;KEY&#125;</span>=<span class="variable">$&#123;VALUE&#125;</span>&quot;</span></span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">  <span class="keyword">done</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">apply_org_vars <span class="string">&quot;OrgA-Dev&quot;</span> <span class="string">&quot;dev&quot;</span></span><br><span class="line">apply_org_vars <span class="string">&quot;OrgA-Stg&quot;</span> <span class="string">&quot;stg&quot;</span></span><br><span class="line">apply_org_vars <span class="string">&quot;OrgA&quot;</span>     <span class="string">&quot;prod&quot;</span></span><br></pre></td></tr></table></figure><hr><h2 id="第五部分：容器构建的工程细节"><a href="#第五部分：容器构建的工程细节" class="headerlink" title="第五部分：容器构建的工程细节"></a>第五部分：容器构建的工程细节</h2><h3 id="多仓库推送设计"><a href="#多仓库推送设计" class="headerlink" title="多仓库推送设计"></a>多仓库推送设计</h3><p><code>image_url_list</code> 是逗号分隔的 URL 列表：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">internet 类型：</span><br><span class="line">  internet.platform-registry.example.com/OrgA/my-app</span><br><span class="line">  + public.platform-registry.example.com/OrgA/my-app</span><br><span class="line"></span><br><span class="line">private 类型：</span><br><span class="line">  internal-private.platform-registry.example.com/OrgA/my-app（仅此一个）</span><br></pre></td></tr></table></figure><h3 id="docker-buildx-imagetools-create-跨仓库复制"><a href="#docker-buildx-imagetools-create-跨仓库复制" class="headerlink" title="docker buildx imagetools create 跨仓库复制"></a><code>docker buildx imagetools create</code> 跨仓库复制</h3><p>构建完成后，使用 <code>imagetools create</code> 直接在 registry 层复制 manifest，<strong>不需要将镜像 pull 到 runner 本地</strong>，节省带宽和时间——在 500+ 仓库的高频构建场景下，这个优化累积效果显著：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 构建并推送到主仓库（primary）</span></span><br><span class="line">docker buildx build \</span><br><span class="line">  --platform linux/amd64 \</span><br><span class="line">  --tag <span class="string">&quot;<span class="variable">$&#123;PRIMARY_URL&#125;</span>:<span class="variable">$&#123;TAG&#125;</span>&quot;</span> \</span><br><span class="line">  --push \</span><br><span class="line">  --file <span class="string">&quot;<span class="variable">$&#123;DOCKERFILE&#125;</span>&quot;</span> .</span><br><span class="line"></span><br><span class="line"><span class="comment"># 跨仓库复制 manifest（仅 manifest，不经过 runner）</span></span><br><span class="line">IFS=<span class="string">&#x27;,&#x27;</span> <span class="built_in">read</span> -ra ALL_URLS &lt;&lt;&lt; <span class="string">&quot;<span class="variable">$&#123;IMAGE_URL_LIST&#125;</span>&quot;</span></span><br><span class="line">PRIMARY_URL=<span class="string">&quot;<span class="variable">$&#123;ALL_URLS[0]&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> EXTRA_URL <span class="keyword">in</span> <span class="string">&quot;<span class="variable">$&#123;ALL_URLS[@]&#125;</span>&quot;</span>; <span class="keyword">do</span></span><br><span class="line">  [ <span class="string">&quot;<span class="variable">$&#123;EXTRA_URL&#125;</span>&quot;</span> = <span class="string">&quot;<span class="variable">$&#123;PRIMARY_URL&#125;</span>&quot;</span> ] &amp;&amp; <span class="built_in">continue</span></span><br><span class="line"></span><br><span class="line">  EXTRA_REGISTRY=$(<span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$&#123;EXTRA_URL&#125;</span>&quot;</span> | <span class="built_in">cut</span> -d<span class="string">&#x27;/&#x27;</span> -f1)</span><br><span class="line">  <span class="keyword">if</span> <span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$&#123;EXTRA_REGISTRY&#125;</span>&quot;</span> | grep -q <span class="string">&#x27;public\.platform&#x27;</span>; <span class="keyword">then</span></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$&#123;PUBLIC_REGISTRY_PASS&#125;</span>&quot;</span> | docker login <span class="string">&quot;<span class="variable">$&#123;EXTRA_REGISTRY&#125;</span>&quot;</span> \</span><br><span class="line">      -u <span class="string">&quot;<span class="variable">$&#123;PUBLIC_REGISTRY_USER&#125;</span>&quot;</span> --password-stdin</span><br><span class="line">  <span class="keyword">else</span></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$&#123;REGISTRY_PASS&#125;</span>&quot;</span> | docker login <span class="string">&quot;<span class="variable">$&#123;EXTRA_REGISTRY&#125;</span>&quot;</span> \</span><br><span class="line">      -u <span class="string">&quot;<span class="variable">$&#123;REGISTRY_USER&#125;</span>&quot;</span> --password-stdin</span><br><span class="line">  <span class="keyword">fi</span></span><br><span class="line"></span><br><span class="line">  docker buildx imagetools create --tag <span class="string">&quot;<span class="variable">$&#123;EXTRA_URL&#125;</span>:<span class="variable">$&#123;TAG&#125;</span>&quot;</span> <span class="string">&quot;<span class="variable">$&#123;PRIMARY_URL&#125;</span>:<span class="variable">$&#123;TAG&#125;</span>&quot;</span></span><br><span class="line">  <span class="built_in">echo</span> <span class="string">&quot;Pushed <span class="variable">$&#123;EXTRA_URL&#125;</span>:<span class="variable">$&#123;TAG&#125;</span>&quot;</span></span><br><span class="line"><span class="keyword">done</span></span><br></pre></td></tr></table></figure><h3 id="镜像签名（Signify）"><a href="#镜像签名（Signify）" class="headerlink" title="镜像签名（Signify）"></a>镜像签名（Signify）</h3><p>平台使用内部 Signify 服务进行镜像签名，采用 mTLS 客户端证书认证。所有 tag × 所有仓库都需要签名：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">IFS=<span class="string">&#x27;,&#x27;</span> <span class="built_in">read</span> -ra ALL_URLS &lt;&lt;&lt; <span class="string">&quot;<span class="variable">$&#123;IMAGE_URL_LIST&#125;</span>&quot;</span></span><br><span class="line">IFS=<span class="string">&#x27;,&#x27;</span> <span class="built_in">read</span> -ra ALL_TAGS &lt;&lt;&lt; <span class="string">&quot;<span class="variable">$&#123;TAG_LIST&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> URL <span class="keyword">in</span> <span class="string">&quot;<span class="variable">$&#123;ALL_URLS[@]&#125;</span>&quot;</span>; <span class="keyword">do</span></span><br><span class="line">  <span class="keyword">for</span> TAG <span class="keyword">in</span> <span class="string">&quot;<span class="variable">$&#123;ALL_TAGS[@]&#125;</span>&quot;</span>; <span class="keyword">do</span></span><br><span class="line">    IMAGE=<span class="string">&quot;<span class="variable">$&#123;URL&#125;</span>:<span class="variable">$&#123;TAG&#125;</span>&quot;</span></span><br><span class="line">    DIGEST=$(docker buildx imagetools inspect <span class="string">&quot;<span class="variable">$&#123;IMAGE&#125;</span>&quot;</span> \</span><br><span class="line">      --format <span class="string">&#x27;&#123;&#123;json .Manifest&#125;&#125;&#x27;</span> | jq -r <span class="string">&#x27;.digest&#x27;</span> | sed <span class="string">&#x27;s/^sha256://&#x27;</span>)</span><br><span class="line">    MANIFEST=$(docker manifest inspect <span class="string">&quot;<span class="variable">$&#123;IMAGE&#125;</span>&quot;</span> 2&gt;/dev/null)</span><br><span class="line">    BYTE_SIZE=$(<span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$&#123;MANIFEST&#125;</span>&quot;</span> | jq -r <span class="string">&#x27;.config.size // 0&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    GUN=$(<span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$&#123;IMAGE&#125;</span>&quot;</span> | rev | <span class="built_in">cut</span> -d<span class="string">&#x27;:&#x27;</span> -f2- | rev)</span><br><span class="line">    PAYLOAD=<span class="string">&quot;&#123;\&quot;trustedCollections\&quot;:[&#123;\&quot;gun\&quot;:\&quot;<span class="variable">$&#123;GUN&#125;</span>\&quot;,\&quot;targets\&quot;:[&#123;\&quot;name\&quot;:\&quot;<span class="variable">$&#123;TAG&#125;</span>\&quot;,\&quot;digest\&quot;:\&quot;<span class="variable">$&#123;DIGEST&#125;</span>\&quot;,\&quot;byteSize\&quot;:<span class="variable">$&#123;BYTE_SIZE&#125;</span>&#125;]&#125;]&#125;&quot;</span></span><br><span class="line"></span><br><span class="line">    curl -sf -X POST \</span><br><span class="line">      --cert <span class="string">&quot;<span class="variable">$&#123;CERT_FILE&#125;</span>&quot;</span> \</span><br><span class="line">      --key  <span class="string">&quot;<span class="variable">$&#123;KEY_FILE&#125;</span>&quot;</span>  \</span><br><span class="line">      --pass <span class="string">&quot;<span class="variable">$&#123;KEY_PASS&#125;</span>&quot;</span>  \</span><br><span class="line">      <span class="string">&quot;<span class="variable">$&#123;SIGNIFY_ENDPOINT&#125;</span>/trusted-collections/publish&quot;</span> \</span><br><span class="line">      -H <span class="string">&quot;Content-Type: application/json&quot;</span> \</span><br><span class="line">      -d <span class="string">&quot;<span class="variable">$&#123;PAYLOAD&#125;</span>&quot;</span> || <span class="built_in">echo</span> <span class="string">&quot;Warning: signing failed for <span class="variable">$&#123;IMAGE&#125;</span>, continuing&quot;</span></span><br><span class="line">  <span class="keyword">done</span></span><br><span class="line"><span class="keyword">done</span></span><br></pre></td></tr></table></figure><h3 id="GHE-上-upload-artifact-v4-的兼容问题"><a href="#GHE-上-upload-artifact-v4-的兼容问题" class="headerlink" title="GHE 上 upload-artifact@v4 的兼容问题"></a>GHE 上 <code>upload-artifact@v4</code> 的兼容问题</h3><p>GitHub Enterprise Server 某些版本不支持 <code>actions/upload-artifact@v4</code> 使用的新 API：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Error: GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+ and</span><br><span class="line">download-artifact@v4+ are not currently supported on GHES.</span><br></pre></td></tr></table></figure><p><strong>在 500+ 仓库规模下，这类兼容性问题的影响面是全量的</strong>——必须降级至 v3：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/upload-artifact@v3</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">lint-report</span></span><br><span class="line">    <span class="attr">path:</span> <span class="string">reports/</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/download-artifact@v3</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">lint-report</span></span><br></pre></td></tr></table></figure><hr><h2 id="第六部分：per-repo-Secrets-—-Vault-Enterprise-Secrets-Sync"><a href="#第六部分：per-repo-Secrets-—-Vault-Enterprise-Secrets-Sync" class="headerlink" title="第六部分：per-repo Secrets — Vault Enterprise Secrets Sync"></a>第六部分：per-repo Secrets — Vault Enterprise Secrets Sync</h2><h3 id="两类凭证的分工"><a href="#两类凭证的分工" class="headerlink" title="两类凭证的分工"></a>两类凭证的分工</h3><table><thead><tr><th>凭证类型</th><th>获取方式</th><th>示例</th><th>安全级别</th></tr></thead><tbody><tr><td>平台共享凭证</td><td>JWT&#x2F;OIDC 运行时从 Vault 拉取</td><td>Registry 凭证、签名证书</td><td>高（跨 Org 权限）</td></tr><tr><td>业务仓库专属凭证</td><td>Vault Secrets Sync 推送为 GitHub Secret</td><td>数据库连接串、业务 API key</td><td>中（单仓库权限）</td></tr></tbody></table><p>在 500+ 仓库规模下，per-repo Secrets 的管理需要自动化——不能手动为 500 个仓库配置。Vault Enterprise Secrets Sync 提供了这个能力。</p><h3 id="Vault-Enterprise-Secrets-Sync-配置"><a href="#Vault-Enterprise-Secrets-Sync-配置" class="headerlink" title="Vault Enterprise Secrets Sync 配置"></a>Vault Enterprise Secrets Sync 配置</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 创建 GitHub Actions 同步目标（Fine-Grained PAT，仅需 secrets:write）</span></span><br><span class="line">vault write sys/sync/destinations/github-actions/my-app-prod \</span><br><span class="line">  access_token=<span class="string">&quot;github_pat_xxxx&quot;</span> \</span><br><span class="line">  repository_owner=<span class="string">&quot;OrgA&quot;</span> \</span><br><span class="line">  repository_name=<span class="string">&quot;my-app&quot;</span> \</span><br><span class="line">  secret_name_template=<span class="string">&quot;&#123;&#123;.SecretKey | uppercase&#125;&#125;&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 创建 Association：KV 路径 → GitHub Actions Secret</span></span><br><span class="line">vault write sys/sync/associations/my-app-prod \</span><br><span class="line">  mount=<span class="string">&quot;secret&quot;</span> \</span><br><span class="line">  secret_name=<span class="string">&quot;apps/my-app/prod/db&quot;</span></span><br></pre></td></tr></table></figure><h3 id="轮换自动同步原理"><a href="#轮换自动同步原理" class="headerlink" title="轮换自动同步原理"></a>轮换自动同步原理</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">Vault KV secret 更新（手动或动态凭证）</span><br><span class="line">      │</span><br><span class="line">      ▼</span><br><span class="line">Vault Sync Engine（后台轮询，约 5 分钟间隔）</span><br><span class="line">      │</span><br><span class="line">      ▼</span><br><span class="line">GitHub API: PUT /repos/OrgA/my-app/actions/secrets/DB_PASSWORD</span><br><span class="line">      │</span><br><span class="line">      ▼</span><br><span class="line">GitHub Secret 自动更新（下次 workflow 运行时生效）</span><br></pre></td></tr></table></figure><p>在 500+ 仓库规模下，凭证轮换不再需要通知每个仓库负责人——Vault Sync Engine 自动完成推送，平台团队只需管理 Vault 中的 KV，业务仓库的 Secret 自动同步更新。</p><hr><h2 id="第七部分：500-规模的可观测性"><a href="#第七部分：500-规模的可观测性" class="headerlink" title="第七部分：500+ 规模的可观测性"></a>第七部分：500+ 规模的可观测性</h2><p>在 500+ 仓库同时运行 CI 的场景下，可观测性不是”加分项”，而是平台运维的基础设施。</p><h3 id="Runner-容量监控"><a href="#Runner-容量监控" class="headerlink" title="Runner 容量监控"></a>Runner 容量监控</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 在每个 job 开始时记录 runner 信息</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Log</span> <span class="string">runner</span> <span class="string">info</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    echo &quot;Runner: $&#123;&#123; runner.name &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;OS: $&#123;&#123; runner.os &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;Arch: $&#123;&#123; runner.arch &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;Repo: $&#123;&#123; github.repository &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;Event: $&#123;&#123; github.event_name &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)&quot;</span></span><br></pre></td></tr></table></figure><h3 id="CI-健康度-Dashboard"><a href="#CI-健康度-Dashboard" class="headerlink" title="CI 健康度 Dashboard"></a>CI 健康度 Dashboard</h3><p>平台团队需要能回答：</p><ul><li>过去 7 天，哪 20 个仓库的 CI 失败率最高？</li><li>平均 CI 耗时趋势（是否有性能退化）？</li><li>安全扫描覆盖率（哪些仓库超过 30 天没有 CI 运行）？</li></ul><p>这些数据可以通过 GitHub API 或 CI 运行时的自定义上报来收集。</p><h3 id="批量合规检查脚本"><a href="#批量合规检查脚本" class="headerlink" title="批量合规检查脚本"></a>批量合规检查脚本</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/usr/bin/env bash</span></span><br><span class="line"><span class="comment"># 检查所有仓库的 CI 接入状态</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;检查 <span class="variable">$&#123;ORG&#125;</span> 下所有仓库...&quot;</span></span><br><span class="line">gh repo list <span class="string">&quot;<span class="variable">$&#123;ORG&#125;</span>&quot;</span> --<span class="built_in">limit</span> 1000 --json name \</span><br><span class="line">  | jq -r <span class="string">&#x27;.[].name&#x27;</span> \</span><br><span class="line">  | <span class="keyword">while</span> <span class="built_in">read</span> repo; <span class="keyword">do</span></span><br><span class="line">      <span class="comment"># 检查是否有 .ci-config/config.yaml</span></span><br><span class="line">      <span class="keyword">if</span> ! gh api <span class="string">&quot;repos/<span class="variable">$&#123;ORG&#125;</span>/<span class="variable">$&#123;repo&#125;</span>/contents/.ci-config/config.yaml&quot;</span> \</span><br><span class="line">          --silent &amp;&gt;/dev/null; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;  [缺少配置] <span class="variable">$&#123;repo&#125;</span>&quot;</span></span><br><span class="line">        <span class="built_in">continue</span></span><br><span class="line">      <span class="keyword">fi</span></span><br><span class="line">      <span class="comment"># 检查是否调用了平台 workflow</span></span><br><span class="line">      <span class="keyword">if</span> ! gh api <span class="string">&quot;repos/<span class="variable">$&#123;ORG&#125;</span>/<span class="variable">$&#123;repo&#125;</span>/contents/.github/workflows/ci.yml&quot;</span> \</span><br><span class="line">          --silent 2&gt;/dev/null | grep -q <span class="string">&#x27;platform-ci-core.yml&#x27;</span>; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;  [未接入平台 CI] <span class="variable">$&#123;repo&#125;</span>&quot;</span></span><br><span class="line">      <span class="keyword">fi</span></span><br><span class="line">    <span class="keyword">done</span></span><br></pre></td></tr></table></figure><hr><h2 id="第八部分：踩坑经验"><a href="#第八部分：踩坑经验" class="headerlink" title="第八部分：踩坑经验"></a>第八部分：踩坑经验</h2><h3 id="1-yq-处理空值的两种姿势"><a href="#1-yq-处理空值的两种姿势" class="headerlink" title="1. yq 处理空值的两种姿势"></a>1. <code>yq</code> 处理空值的两种姿势</h3><p><code>yq</code> 在字段不存在时默认输出字符串 <code>null</code>，导致下游 job 拿到字面量 <code>&quot;null&quot;</code>。在 500+ 仓库中，各种 <code>config.yaml</code> 的写法差异很大，这类问题出现频率高：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 方案 A：yq 内联默认值（推荐，简单标量字段）</span></span><br><span class="line">RCFILE=$(yq <span class="string">&#x27;.jobs[].pyLint.rcFile // &quot;&quot;&#x27;</span> config.yaml)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 方案 B：shell 过滤（处理所有 null 输出场景）</span></span><br><span class="line">RCFILE=$(yq <span class="string">&#x27;.jobs[].pyLint.rcFile&#x27;</span> config.yaml | grep -v <span class="string">&#x27;^null$&#x27;</span> || <span class="built_in">echo</span> <span class="string">&quot;&quot;</span>)</span><br></pre></td></tr></table></figure><h3 id="2-GITHUB-OUTPUT-多行值必须用-heredoc"><a href="#2-GITHUB-OUTPUT-多行值必须用-heredoc" class="headerlink" title="2. $GITHUB_OUTPUT 多行值必须用 heredoc"></a>2. <code>$GITHUB_OUTPUT</code> 多行值必须用 heredoc</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 错误：换行截断</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;changelog=<span class="variable">$&#123;MULTI_LINE_TEXT&#125;</span>&quot;</span> &gt;&gt; <span class="string">&quot;<span class="variable">$GITHUB_OUTPUT</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 正确：heredoc 格式</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="built_in">echo</span> <span class="string">&quot;changelog&lt;&lt;EOF&quot;</span></span><br><span class="line">  <span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$&#123;MULTI_LINE_TEXT&#125;</span>&quot;</span></span><br><span class="line">  <span class="built_in">echo</span> <span class="string">&quot;EOF&quot;</span></span><br><span class="line">&#125; &gt;&gt; <span class="string">&quot;<span class="variable">$GITHUB_OUTPUT</span>&quot;</span></span><br></pre></td></tr></table></figure><h3 id="3-Reusable-workflow-的-with-字段只支持字符串"><a href="#3-Reusable-workflow-的-with-字段只支持字符串" class="headerlink" title="3. Reusable workflow 的 with: 字段只支持字符串"></a>3. Reusable workflow 的 <code>with:</code> 字段只支持字符串</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">inputs:</span></span><br><span class="line">  <span class="attr">has_unit_test:</span></span><br><span class="line">    <span class="attr">type:</span> <span class="string">string</span>   <span class="comment"># 只能是 string，不能是 boolean</span></span><br><span class="line"></span><br><span class="line"><span class="attr">steps:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Run</span> <span class="string">unit</span> <span class="string">tests</span></span><br><span class="line">    <span class="attr">if:</span> <span class="string">inputs.has_unit_test</span> <span class="string">==</span> <span class="string">&#x27;true&#x27;</span>   <span class="comment"># 字符串比较</span></span><br><span class="line">    <span class="attr">run:</span> <span class="string">pytest</span></span><br></pre></td></tr></table></figure><h3 id="4-pull-request-target-checkout-的安全陷阱"><a href="#4-pull-request-target-checkout-的安全陷阱" class="headerlink" title="4. pull_request_target + checkout 的安全陷阱"></a>4. <code>pull_request_target</code> + checkout 的安全陷阱</h3><p><code>pull_request_target</code> 在 base 仓库上下文运行，但 <code>actions/checkout</code> 默认 checkout <strong>base branch</strong>，扫描的不是 PR 的代码。</p><p>必须显式指定 PR head SHA：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">ref:</span> <span class="string">$&#123;&#123;</span> <span class="string">github.event.pull_request.head.sha</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><p>安全注意：checkout 的是 fork 的代码，Secrets 访问逻辑必须与代码 checkout 隔离在不同 job，防止 fork 代码中的恶意脚本读取 Secrets。</p><h3 id="5-needs-config-outputs-在-if-条件里的正确写法"><a href="#5-needs-config-outputs-在-if-条件里的正确写法" class="headerlink" title="5. needs.config.outputs 在 if: 条件里的正确写法"></a>5. <code>needs.config.outputs</code> 在 <code>if:</code> 条件里的正确写法</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">build:</span></span><br><span class="line">  <span class="attr">needs:</span> <span class="string">config</span></span><br><span class="line">  <span class="comment"># 正确：完整的表达式语法</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">$&#123;&#123;</span> <span class="string">needs.config.outputs.dockerfile</span> <span class="type">!=</span> <span class="string">&#x27;&#x27;</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br><span class="line">  <span class="comment"># 错误：非 $&#123;&#123; &#125;&#125; 包裹时，非空字符串不自动为 true</span></span><br><span class="line">  <span class="comment"># if: needs.config.outputs.dockerfile</span></span><br></pre></td></tr></table></figure><h3 id="6-500-仓库并发触发时的-runner-队列压力"><a href="#6-500-仓库并发触发时的-runner-队列压力" class="headerlink" title="6. 500+ 仓库并发触发时的 runner 队列压力"></a>6. 500+ 仓库并发触发时的 runner 队列压力</h3><p>早上提交高峰期，runner 队列可能积压数百个 job。需要监控 <code>queue_time</code>（job 进入队列到开始运行的时间），并据此调整 runner 数量。超过 5 分钟的队列等待会显著影响开发者体验。</p><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在 500+ 仓库规模下，GitHub Actions Reusable Workflow 方案的核心价值：</p><ol><li><strong>静态结构限制的绕过</strong>：<code>config</code> job 作为动态配置中间层，替代 Jenkins Groovy 的运行时合并能力</li><li><strong>JWT&#x2F;OIDC 零长期凭证</strong>：500 个仓库无一存储凭证，batch token 不可查询，安全边界最大化</li><li><strong>多环境零代码路由</strong>：Organization Variable 注入，三套环境用完全相同的 workflow 代码</li><li><strong>多仓库容器构建</strong>：<code>imagetools create</code> 的 manifest 层复制，节省 500+ 仓库每天数千次构建的带宽</li><li><strong>凭证分层管理</strong>：平台共享凭证走 OIDC，业务专属凭证走 Secrets Sync，500 个仓库的凭证生命周期自动化</li></ol><p>每个业务仓库最终只需维护 15 行 <code>ci.yml</code> 和一个 <code>.ci-config/config.yaml</code>——这个”最简接入模型”在第 1 个仓库和第 500 个仓库上完全相同，这正是规模化平台的设计目标。</p>]]>
    </content>
    <id>https://www.realks.com/2026/06/21/unified-cicd-03-github-actions/</id>
    <link href="https://www.realks.com/2026/06/21/unified-cicd-03-github-actions/"/>
    <published>2026-06-21T02:20:00.000Z</published>
    <summary>
      <![CDATA[<h1 id="GitHub-Actions-Reusable-Workflow：零配置统一-CI-CD-的完整实现"><a href="#GitHub-Actions-Reusable-Workflow：零配置统一-CI-CD-的完整实现" class="headerlink" title="GitHub Actions Reusable Workflow：零配置统一 CI&#x2F;CD 的完整实现"></a>GitHub Actions Reusable Workflow：零配置统一 CI&#x2F;CD 的完整实现</h1><blockquote>
<p>本文是《统一 CI&#x2F;CD 流水线治理》系列第三篇。本文深入拆解平台团队如何用 Reusable Workflow 实现”业务仓库零配置接入”的完整方案，涵盖架构设计、JWT&#x2F;OIDC 认证、多环境路由、容器构建以及踩坑记录。本文来自一个覆盖 500+ 仓库、生产运行中的实践。</p>
</blockquote>
<hr>]]>
    </summary>
    <title>GitHub Actions Reusable Workflow：零配置统一 CI/CD 的完整实现</title>
    <updated>2026-06-21T02:20:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="DevOps" scheme="https://www.realks.com/categories/DevOps/"/>
    <category term="CI/CD" scheme="https://www.realks.com/categories/DevOps/CI-CD/"/>
    <category term="Jenkins" scheme="https://www.realks.com/tags/Jenkins/"/>
    <category term="CI/CD" scheme="https://www.realks.com/tags/CI-CD/"/>
    <category term="Platform Engineering" scheme="https://www.realks.com/tags/Platform-Engineering/"/>
    <category term="Shared Library" scheme="https://www.realks.com/tags/Shared-Library/"/>
    <category term="Groovy" scheme="https://www.realks.com/tags/Groovy/"/>
    <content>
      <![CDATA[<h1 id="Jenkins-Shared-Library：统一流水线的工程实现"><a href="#Jenkins-Shared-Library：统一流水线的工程实现" class="headerlink" title="Jenkins Shared Library：统一流水线的工程实现"></a>Jenkins Shared Library：统一流水线的工程实现</h1><blockquote><p>本文是《统一 CI&#x2F;CD 流水线治理》系列第二篇。上一篇讲了为什么要统一管理，这篇深入 Jenkins Shared Library 的技术实现细节。本文来自一个覆盖 500+ 仓库、运行 2 年以上的生产实践。</p></blockquote><hr><span id="more"></span><h2 id="一、Shared-Library-是什么"><a href="#一、Shared-Library-是什么" class="headerlink" title="一、Shared Library 是什么"></a>一、Shared Library 是什么</h2><p>Jenkins Shared Library 是 Jenkins 提供的代码复用机制：将 Groovy 代码放在独立 Git 仓库中，在 Jenkins 全局配置中注册后，所有 Jenkinsfile 都可以 <code>@Library</code> 引入并调用其中的函数。</p><p>对业务团队来说，效果是这样的：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 业务仓库的 Jenkinsfile（完整文件）</span></span><br><span class="line"><span class="meta">@Library</span>(<span class="string">&#x27;platform-ci-library&#x27;</span>) _</span><br><span class="line"></span><br><span class="line">platformCi()</span><br></pre></td></tr></table></figure><p>两行代码，完整的 CI&#x2F;CD 流水线。平台团队在 <code>platform-ci-library</code> 仓库中维护所有逻辑，500+ 个业务仓库都是这两行。</p><hr><h2 id="二、Shared-Library-目录结构"><a href="#二、Shared-Library-目录结构" class="headerlink" title="二、Shared Library 目录结构"></a>二、Shared Library 目录结构</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">platform-ci-library/</span><br><span class="line">├── vars/</span><br><span class="line">│   └── platformCi.groovy          # 业务仓库调用的入口函数</span><br><span class="line">├── src/</span><br><span class="line">│   └── com/platform/ci/</span><br><span class="line">│       ├── ConfigMerger.groovy    # 配置合并逻辑</span><br><span class="line">│       ├── PodGenerator.groovy    # Kubernetes Pod YAML 生成</span><br><span class="line">│       ├── StageGenerator.groovy  # 动态 Stage 生成</span><br><span class="line">│       └── VaultClient.groovy     # Vault AppRole 认证</span><br><span class="line">└── resources/</span><br><span class="line">    └── config/</span><br><span class="line">        └── default.yaml           # 平台默认配置</span><br></pre></td></tr></table></figure><p><strong>三个目录的职责：</strong></p><ul><li><code>vars/</code>：存放全局变量和顶层函数，文件名即函数名（<code>platformCi.groovy</code> → <code>platformCi()</code>）</li><li><code>src/</code>：存放辅助类，遵循 Java 包路径约定，可以使用完整的 Groovy&#x2F;Java 语法</li><li><code>resources/</code>：存放静态资源文件，通过 <code>libraryResource()</code> 加载</li></ul><hr><h2 id="三、入口函数的完整执行流程"><a href="#三、入口函数的完整执行流程" class="headerlink" title="三、入口函数的完整执行流程"></a>三、入口函数的完整执行流程</h2><p><code>vars/platformCi.groovy</code> 是整个流水线的编排入口：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// vars/platformCi.groovy（伪代码，已脱敏）</span></span><br><span class="line"><span class="keyword">def</span> call(Map params = [:]) &#123;</span><br><span class="line">    <span class="comment">// Step 1: 加载平台默认配置</span></span><br><span class="line">    <span class="keyword">def</span> defaultConfigYaml = libraryResource(<span class="string">&#x27;config/default.yaml&#x27;</span>)</span><br><span class="line">    <span class="keyword">def</span> defaultConfig = readYaml(<span class="attr">text:</span> defaultConfigYaml)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Step 2: 加载业务仓库的 .ci-config/config.yaml</span></span><br><span class="line">    <span class="keyword">def</span> repoConfig = readYaml(<span class="attr">file:</span> <span class="string">&#x27;.ci-config/config.yaml&#x27;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Step 3: 合并配置（平台默认 + 业务覆盖）</span></span><br><span class="line">    <span class="keyword">def</span> mergedConfig = <span class="keyword">new</span> ConfigMerger().run(defaultConfig, repoConfig)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Step 4: 自动提取仓库名（从 Git 远端 URL，无需业务团队传入）</span></span><br><span class="line">    <span class="keyword">def</span> repoName = sh(</span><br><span class="line">        <span class="symbol">script:</span> <span class="string">&quot;git remote get-url origin | sed &#x27;s|.*[:/]||&#x27; | sed &#x27;s|\\.git$||&#x27;&quot;</span>,</span><br><span class="line">        <span class="symbol">returnStdout:</span> <span class="literal">true</span></span><br><span class="line">    ).trim()</span><br><span class="line">    mergedConfig.repoName = repoName</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Step 5: 生成 Kubernetes Pod YAML</span></span><br><span class="line">    <span class="keyword">def</span> podYaml = <span class="keyword">new</span> PodGenerator().run(mergedConfig.containers)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Step 6: Vault AppRole 认证，换取临时 token</span></span><br><span class="line">    <span class="keyword">def</span> vaultToken = <span class="keyword">new</span> VaultClient().getToken(mergedConfig.vault)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Step 7: 动态生成并运行 stage</span></span><br><span class="line">    podTemplate(<span class="attr">yaml:</span> podYaml) &#123;</span><br><span class="line">        node(POD_LABEL) &#123;</span><br><span class="line">            checkout scm</span><br><span class="line">            <span class="keyword">new</span> StageGenerator().run(mergedConfig, vaultToken)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Step 8: post 阶段（状态上报、健康检查数据推送）</span></span><br><span class="line">    <span class="comment">// 注：Jenkins post 块在 StageGenerator 外部处理</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>为什么要自动提取仓库名？</strong></p><p>要求 500 个业务团队在 <code>platformCi()</code> 调用时传入仓库名，100% 会有人拼写错误或大小写不一致。从 Git remote URL 提取是无歧义的：<code>https://github.example.com/OrgA/my-app.git</code> → <code>my-app</code>。</p><hr><h2 id="四、配置合并机制"><a href="#四、配置合并机制" class="headerlink" title="四、配置合并机制"></a>四、配置合并机制</h2><h3 id="4-1-default-yaml-的结构设计"><a href="#4-1-default-yaml-的结构设计" class="headerlink" title="4.1 default.yaml 的结构设计"></a>4.1 <code>default.yaml</code> 的结构设计</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># resources/config/default.yaml</span></span><br><span class="line"><span class="attr">containers:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">jnlp</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">platform-registry.example.com/jenkins-inbound-agent:latest</span></span><br><span class="line">    <span class="attr">allowOverride:</span> <span class="literal">false</span>       <span class="comment"># 业务团队不能覆盖这个容器</span></span><br><span class="line"></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">project-runtime</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">platform-registry.example.com/python:3.x</span></span><br><span class="line">    <span class="attr">allowOverride:</span> <span class="literal">true</span>        <span class="comment"># 业务团队可以指定 Python 版本</span></span><br><span class="line"></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">build-tools</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">platform-registry.example.com/build-tools:latest</span></span><br><span class="line">    <span class="attr">allowOverride:</span> <span class="literal">false</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">security-scan</span></span><br><span class="line">    <span class="attr">allowOverride:</span> <span class="literal">false</span>       <span class="comment"># 安全扫描不允许关闭（合规基线）</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">semgrep:</span></span><br><span class="line">          <span class="attr">rulesets:</span> [<span class="string">&quot;p/python&quot;</span>, <span class="string">&quot;p/security-audit&quot;</span>]</span><br><span class="line"></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">lint</span></span><br><span class="line">    <span class="attr">allowOverride:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">pyLint:</span></span><br><span class="line">          <span class="attr">sourceSets:</span> []       <span class="comment"># 默认空，业务仓库必须声明</span></span><br><span class="line">          <span class="attr">rcFile:</span> <span class="string">&quot;&quot;</span>           <span class="comment"># 默认空，使用平台 rcFile</span></span><br><span class="line"></span><br><span class="line"><span class="attr">vault:</span></span><br><span class="line">  <span class="attr">address:</span> <span class="string">https://vault.example.com</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">platform/projects/myteam</span></span><br><span class="line">  <span class="attr">roleIds:</span></span><br><span class="line">    <span class="attr">OrgA-Dev:</span> <span class="string">&quot;role-id-dev-placeholder&quot;</span></span><br><span class="line">    <span class="attr">OrgA-Stg:</span> <span class="string">&quot;role-id-stg-placeholder&quot;</span></span><br><span class="line">    <span class="attr">OrgA:</span>     <span class="string">&quot;role-id-prod-placeholder&quot;</span></span><br></pre></td></tr></table></figure><h3 id="4-2-合并规则"><a href="#4-2-合并规则" class="headerlink" title="4.2 合并规则"></a>4.2 合并规则</h3><p>配置合并需要处理两类数据结构：</p><p><strong>标量字段（字符串、数字、布尔）</strong>：业务值直接覆盖默认值（如果 <code>allowOverride: true</code>）</p><p><strong>列表字段（如 <code>containers</code>）</strong>：按 <code>name</code> 字段匹配合并，而非简单追加或替换</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/com/platform/ci/ConfigMerger.groovy（核心逻辑，已简化）</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ConfigMerger</span> &#123;</span><br><span class="line">    Map run(Map defaultConfig, Map repoConfig) &#123;</span><br><span class="line">        <span class="keyword">def</span> merged = deepCopy(defaultConfig)</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 合并 containers：按 name 匹配</span></span><br><span class="line">        repoConfig.containers?.each &#123; repoContainer -&gt;</span><br><span class="line">            def defaultContainer = merged.containers.find &#123;</span><br><span class="line">                it.name == repoContainer.name</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            if (defaultContainer) &#123;</span><br><span class="line">                if (defaultContainer.allowOverride == false) &#123;</span><br><span class="line">                    <span class="comment">// 平台强制容器，忽略业务覆盖，打印警告</span></span><br><span class="line">                    echo <span class="string">&quot;Warning: container &#x27;$&#123;repoContainer.name&#125;&#x27; has allowOverride=false, ignoring override&quot;</span></span><br><span class="line">                &#125; else &#123;</span><br><span class="line">                    <span class="comment">// 合并字段（不允许覆盖 allowOverride 字段本身）</span></span><br><span class="line">                    repoContainer.each &#123; key, value -&gt;</span><br><span class="line">                        if (key != <span class="string">&#x27;allowOverride&#x27;</span>) &#123;</span><br><span class="line">                            defaultContainer[key] = value</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; else &#123;</span><br><span class="line">                <span class="comment">// 业务仓库声明了新容器，追加</span></span><br><span class="line">                merged.containers &lt;&lt; repoContainer</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 合并 jobs：按 name 匹配，逻辑类似</span></span><br><span class="line">        repoConfig.jobs?.each &#123; repoJob -&gt;</span><br><span class="line">            def defaultJob = merged.jobs.find &#123; it.name == repoJob.name &#125;</span><br><span class="line">            if (defaultJob &amp;&amp; defaultJob.allowOverride == false) &#123;</span><br><span class="line">                return</span><br><span class="line">            &#125;</span><br><span class="line">            mergeJob(merged.jobs, repoJob)</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        return merged</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="4-3-Python-版本提取"><a href="#4-3-Python-版本提取" class="headerlink" title="4.3 Python 版本提取"></a>4.3 Python 版本提取</h3><p>业务团队声明的是镜像 tag，不是 Python 版本号：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">containers:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">project-runtime</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">platform-registry.example.com/python:3.14</span></span><br></pre></td></tr></table></figure><p>平台从镜像 tag 中提取版本：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> pythonVersion = repoContainer.image</span><br><span class="line">    .tokenize(<span class="string">&#x27;:&#x27;</span>)</span><br><span class="line">    .last()               <span class="comment">// &quot;3.14&quot;</span></span><br><span class="line">    .find(<span class="regexp">/\d+\.\d+(\.\d+)?/</span>)  <span class="comment">// 正则提取语义化版本</span></span><br></pre></td></tr></table></figure><p><code>3.14</code> → 用于 <code>pip install</code>、<code>python --version</code> 验证、lint 配置中的 <code>python-version</code>。</p><p>在 500+ 仓库中，Python 版本跨度通常从 3.8 到 3.13，平台需要兼容所有版本，而不是要求业务团队主动传入版本号。</p><hr><h2 id="五、动态-Stage-生成"><a href="#五、动态-Stage-生成" class="headerlink" title="五、动态 Stage 生成"></a>五、动态 Stage 生成</h2><p>这是 Jenkins 方案最独特的能力，也是 GitHub Actions 最难复制的部分。</p><h3 id="5-1-什么是”运行时动态-Stage”"><a href="#5-1-什么是”运行时动态-Stage”" class="headerlink" title="5.1 什么是”运行时动态 Stage”"></a>5.1 什么是”运行时动态 Stage”</h3><p>在 Jenkins Pipeline 中，stage 可以在 Groovy 代码执行时动态创建：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// StageGenerator 根据配置决定运行哪些 stage</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">StageGenerator</span> &#123;</span><br><span class="line">    <span class="type">void</span> run(Map config, String vaultToken) &#123;</span><br><span class="line">        stage(<span class="string">&#x27;Checkout&#x27;</span>) &#123;</span><br><span class="line">            checkout scm</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 有 pylint 配置时才运行</span></span><br><span class="line">        <span class="keyword">if</span> (config.jobs.find &#123; it.name == <span class="string">&#x27;lint&#x27;</span> &#125;?.steps?.pyLint?.sourceSets) &#123;</span><br><span class="line">            stage(<span class="string">&#x27;Lint&#x27;</span>) &#123;</span><br><span class="line">                runPylint(config, vaultToken)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 有 unit-test job 时才运行</span></span><br><span class="line">        if (config.jobs.find &#123; it.name == <span class="string">&#x27;unit-test&#x27;</span> &#125;) &#123;</span><br><span class="line">            stage(<span class="string">&#x27;Unit Test&#x27;</span>) &#123;</span><br><span class="line">                runUnitTest(config, vaultToken)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 总是运行，allowOverride=false</span></span><br><span class="line">        stage(<span class="string">&#x27;Security Scan&#x27;</span>) &#123;</span><br><span class="line">            runSecurityScan(config, vaultToken)</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 有 Dockerfile 时才运行</span></span><br><span class="line">        if (config.containerBuild?.path) &#123;</span><br><span class="line">            stage(<span class="string">&#x27;Build&#x27;</span>) &#123;</span><br><span class="line">                runContainerBuild(config, vaultToken)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 业务仓库在 config.yaml 中声明的额外任务（500+ 仓库中有几十种自定义 stage）</span></span><br><span class="line">        config.jobs.findAll &#123; it.name.startsWith(<span class="string">&#x27;custom-&#x27;</span>) &#125;.each &#123; customJob -&gt;</span><br><span class="line">            stage(customJob.name) &#123;</span><br><span class="line">                runCustomJob(customJob, vaultToken)</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>在 500+ 仓库规模下，这个能力尤为重要</strong>：不同仓库的 stage 结构差异很大，有的仓库有 3 个 stage，有的有 12 个（含多个自定义 stage）。Jenkins 天然支持这种动态结构，业务仓库只需在 <code>config.yaml</code> 中声明，无需修改平台代码。</p><h3 id="5-2-并行-Stage"><a href="#5-2-并行-Stage" class="headerlink" title="5.2 并行 Stage"></a>5.2 并行 Stage</h3><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">stage(<span class="string">&#x27;Parallel Checks&#x27;</span>) &#123;</span><br><span class="line">    parallel(</span><br><span class="line">        <span class="string">&#x27;Lint&#x27;</span>: &#123;</span><br><span class="line">            runPylint(config, vaultToken)</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&#x27;Security Scan&#x27;</span>: &#123;</span><br><span class="line">            runSecurityScan(config, vaultToken)</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&#x27;Unit Tests&#x27;</span>: &#123;</span><br><span class="line">            runUnitTest(config, vaultToken)</span><br><span class="line">        &#125;</span><br><span class="line">    )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>并行度在运行时动态决定——没有单元测试的仓库，<code>parallel</code> 块中就不会有 <code>Unit Tests</code> 分支。</p><h3 id="5-3-GitHub-Actions-的对比局限"><a href="#5-3-GitHub-Actions-的对比局限" class="headerlink" title="5.3 GitHub Actions 的对比局限"></a>5.3 GitHub Actions 的对比局限</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># GitHub Actions：无法做到&quot;有 Dockerfile 才显示 Build job&quot;</span></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">build:</span></span><br><span class="line">    <span class="attr">if:</span> <span class="string">$&#123;&#123;</span> <span class="string">needs.config.outputs.dockerfile</span> <span class="type">!=</span> <span class="string">&#x27;&#x27;</span> <span class="string">&#125;&#125;</span>  <span class="comment"># 可以跳过，但 job 始终出现在 UI 中</span></span><br><span class="line">    <span class="attr">uses:</span> <span class="string">./.github/workflows/platform-ci-build.yml</span></span><br></pre></td></tr></table></figure><p>GitHub Actions 能跳过 job，但 job 的定义是静态的。对 95% 的场景，”跳过”和”不存在”没有区别；但对于需要动态声明任意数量自定义 stage 的业务仓库，Jenkins 方案更自然。</p><hr><h2 id="六、Vault-AppRole-凭证管理"><a href="#六、Vault-AppRole-凭证管理" class="headerlink" title="六、Vault AppRole 凭证管理"></a>六、Vault AppRole 凭证管理</h2><h3 id="6-1-AppRole-认证流程"><a href="#6-1-AppRole-认证流程" class="headerlink" title="6.1 AppRole 认证流程"></a>6.1 AppRole 认证流程</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">Jenkins Credential Store</span><br><span class="line">├── RoleID（低敏感性，可以在配置文件中硬编码）</span><br><span class="line">└── SecretID（高敏感性，保存在 Jenkins Credential Store，定期轮换）</span><br><span class="line">           │</span><br><span class="line">           ▼</span><br><span class="line">POST /v1/auth/approle/login</span><br><span class="line">&#123; &quot;role_id&quot;: &quot;...&quot;, &quot;secret_id&quot;: &quot;...&quot; &#125;</span><br><span class="line">           │</span><br><span class="line">           ▼</span><br><span class="line">临时 Vault Token（TTL: 1小时）</span><br><span class="line">           │</span><br><span class="line">           ▼</span><br><span class="line">读取 KV secrets（Registry 凭证、代码签名证书等）</span><br></pre></td></tr></table></figure><h3 id="6-2-多环境路由"><a href="#6-2-多环境路由" class="headerlink" title="6.2 多环境路由"></a>6.2 多环境路由</h3><p>不同 GitHub Org 对应不同环境，RoleID 不同：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/com/platform/ci/VaultClient.groovy</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">VaultClient</span> &#123;</span><br><span class="line">    String getToken(Map vaultConfig) &#123;</span><br><span class="line">        <span class="keyword">def</span> remoteUrl = sh(</span><br><span class="line">            <span class="symbol">script:</span> <span class="string">&#x27;git remote get-url origin&#x27;</span>,</span><br><span class="line">            <span class="symbol">returnStdout:</span> <span class="literal">true</span></span><br><span class="line">        ).trim()</span><br><span class="line"></span><br><span class="line">        <span class="keyword">def</span> roleId = resolveRoleId(remoteUrl, vaultConfig.roleIds)</span><br><span class="line">        <span class="keyword">def</span> secretId = getSecretId()  <span class="comment">// 从 Jenkins Credential Store 读取</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">def</span> response = httpRequest(</span><br><span class="line">            <span class="symbol">url:</span> <span class="string">&quot;$&#123;vaultConfig.address&#125;/v1/auth/approle/login&quot;</span>,</span><br><span class="line">            <span class="symbol">httpMode:</span> <span class="string">&#x27;POST&#x27;</span>,</span><br><span class="line">            <span class="symbol">contentType:</span> <span class="string">&#x27;APPLICATION_JSON&#x27;</span>,</span><br><span class="line">            <span class="symbol">requestBody:</span> <span class="string">&quot;&quot;&quot;&#123;&quot;role_id&quot;:&quot;$&#123;roleId&#125;&quot;,&quot;secret_id&quot;:&quot;$&#123;secretId&#125;&quot;&#125;&quot;&quot;&quot;</span></span><br><span class="line">        )</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> readJSON(<span class="attr">text:</span> response.content).auth.client_token</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String resolveRoleId(String remoteUrl, Map roleIds) &#123;</span><br><span class="line">        <span class="keyword">if</span> (remoteUrl.contains(<span class="string">&#x27;OrgA-Dev&#x27;</span>)) <span class="keyword">return</span> roleIds[<span class="string">&#x27;OrgA-Dev&#x27;</span>]</span><br><span class="line">        <span class="keyword">if</span> (remoteUrl.contains(<span class="string">&#x27;OrgA-Stg&#x27;</span>)) <span class="keyword">return</span> roleIds[<span class="string">&#x27;OrgA-Stg&#x27;</span>]</span><br><span class="line">        <span class="keyword">return</span> roleIds[<span class="string">&#x27;OrgA&#x27;</span>]  <span class="comment">// 默认 prod</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String getSecretId() &#123;</span><br><span class="line">        withCredentials([string(<span class="attr">credentialsId:</span> <span class="string">&#x27;vault-approle-secret-id&#x27;</span>, <span class="attr">variable:</span> <span class="string">&#x27;SECRET_ID&#x27;</span>)]) &#123;</span><br><span class="line">            <span class="keyword">return</span> env.SECRET_ID</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-3-凭证注入到-Stage-环境变量"><a href="#6-3-凭证注入到-Stage-环境变量" class="headerlink" title="6.3 凭证注入到 Stage 环境变量"></a>6.3 凭证注入到 Stage 环境变量</h3><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> registryCreds = readVaultSecret(vaultToken, <span class="string">&#x27;secret/data/platform/dev/registry&#x27;</span>)</span><br><span class="line"></span><br><span class="line">container(<span class="string">&#x27;build-tools&#x27;</span>) &#123;</span><br><span class="line">    withEnv([</span><br><span class="line">        <span class="string">&quot;REGISTRY_USER=$&#123;registryCreds.username&#125;&quot;</span>,</span><br><span class="line">        <span class="string">&quot;REGISTRY_PASS=$&#123;registryCreds.password&#125;&quot;</span></span><br><span class="line">    ]) &#123;</span><br><span class="line">        sh <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">            echo &quot;\$&#123;REGISTRY_PASS&#125;&quot; | docker login platform-registry.example.com \\</span></span><br><span class="line"><span class="string">              -u &quot;\$&#123;REGISTRY_USER&#125;&quot; --password-stdin</span></span><br><span class="line"><span class="string">            docker build -t my-app:latest .</span></span><br><span class="line"><span class="string">            docker push my-app:latest</span></span><br><span class="line"><span class="string">        &quot;&quot;&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-4-AppRole-在-500-规模下的安全隐患"><a href="#6-4-AppRole-在-500-规模下的安全隐患" class="headerlink" title="6.4 AppRole 在 500+ 规模下的安全隐患"></a>6.4 AppRole 在 500+ 规模下的安全隐患</h3><p>AppRole 方案的已知局限，在 500+ 仓库规模下被放大：</p><ol><li><strong>SecretID 全局共享</strong>：所有 500 个仓库的 CI 共享同一个 SecretID，任何一个仓库的 Groovy 代码理论上都能通过 <code>sh &#39;printenv | grep VAULT&#39;</code> 读取注入的凭证</li><li><strong>轮换协调成本高</strong>：SecretID 轮换时需要暂停全部 CI 或容忍短暂认证失败，在 500+ 仓库的高频 CI 环境中，轮换窗口的影响面很大</li><li><strong>Token 全 Pipeline 共享</strong>：单次 Pipeline 运行的所有 stage 共享同一个 1 小时 TTL 的 Vault token</li></ol><p>这些不是 Jenkins 的根本性缺陷，但在 500+ 规模下，维持同等安全水平所需的运维投入明显高于 GitHub Actions 的 JWT&#x2F;OIDC 方案（无静态凭证、每个 sub-workflow 独立 5 分钟 batch token）。</p><hr><h2 id="七、500-规模下的运维挑战"><a href="#七、500-规模下的运维挑战" class="headerlink" title="七、500+ 规模下的运维挑战"></a>七、500+ 规模下的运维挑战</h2><h3 id="7-1-Jenkins-主节点-OOM"><a href="#7-1-Jenkins-主节点-OOM" class="headerlink" title="7.1 Jenkins 主节点 OOM"></a>7.1 Jenkins 主节点 OOM</h3><p>500+ 仓库并发提交时（如早上 9 点开工高峰），同时运行的 Pipeline 数量可能达到 100-200 个。每个运行中的 Pipeline 都在 Jenkins 主节点 JVM 中占用内存（用于存储 Pipeline 状态）。</p><p>典型症状：Jenkins UI 响应变慢 → 新 Pipeline 无法启动 → 已有 Pipeline 被强制终止 → JVM 崩溃重启。</p><p><strong>在 500+ 规模下，这不是偶发问题，而是一个需要持续运维的系统压力。</strong></p><p>缓解措施：</p><ul><li>配置 Pipeline Durability 为 <code>PERFORMANCE_OPTIMIZED</code>（减少状态保存频率）</li><li>增大 Jenkins 主节点 JVM 堆（<code>-Xmx</code>），通常需要 16GB+</li><li>限制最大并发 Pipeline 数量（Throttle Concurrent Builds 插件）</li><li>将 Pipeline 日志存储外置（不存在主节点磁盘上）</li><li>使用 <code>@NonCPS</code> 减少序列化对象数量</li></ul><h3 id="7-2-NonCPS-注解陷阱"><a href="#7-2-NonCPS-注解陷阱" class="headerlink" title="7.2 @NonCPS 注解陷阱"></a>7.2 <code>@NonCPS</code> 注解陷阱</h3><p>Jenkins Pipeline 的 Groovy 代码需要支持序列化（将执行状态保存到磁盘以便恢复）。普通 Groovy 对象大多不可序列化，这导致常见错误：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">NotSerializableException: java.util.LinkedHashMap</span><br></pre></td></tr></table></figure><p>解决方案是用 <code>@NonCPS</code> 标注不需要序列化的方法，但 <code>@NonCPS</code> 方法中不能使用 Pipeline DSL：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 错误：普通方法中使用了不可序列化的对象</span></span><br><span class="line"><span class="keyword">def</span> processConfig(Map config) &#123;</span><br><span class="line">    config.entrySet().each &#123; entry -&gt;  <span class="comment">// entrySet() 返回不可序列化的视图</span></span><br><span class="line">        <span class="comment">// ...</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 正确：用 @NonCPS 标注，方法中不使用 Pipeline DSL</span></span><br><span class="line"><span class="meta">@NonCPS</span></span><br><span class="line">List processConfigKeys(Map config) &#123;</span><br><span class="line">    <span class="keyword">return</span> config.keySet().toList()  <span class="comment">// 返回可序列化的 List</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在维护大型 Shared Library 时，这个问题会在每次功能迭代中反复出现。处理策略：所有纯数据处理方法加 <code>@NonCPS</code>，所有 Pipeline DSL 调用（<code>sh</code>、<code>stage</code>、<code>echo</code>）不加。</p><h3 id="7-3-Kubernetes-Plugin-升级破坏性变更"><a href="#7-3-Kubernetes-Plugin-升级破坏性变更" class="headerlink" title="7.3 Kubernetes Plugin 升级破坏性变更"></a>7.3 Kubernetes Plugin 升级破坏性变更</h3><p>Jenkins Kubernetes Plugin 在某些版本升级后，Pod YAML 的字段格式会变化，导致 Pod 调度失败——在 500+ 仓库环境中，这意味着全量 CI 中断。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"># 新版本要求 containers 有明确的 resources 字段，否则 Pod 调度失败</span><br><span class="line">spec:</span><br><span class="line">  containers:</span><br><span class="line">  - name: jnlp</span><br><span class="line">    image: jenkins/inbound-agent:latest</span><br><span class="line">    resources:</span><br><span class="line">      requests:</span><br><span class="line">        memory: &quot;256Mi&quot;</span><br><span class="line">        cpu: &quot;100m&quot;</span><br></pre></td></tr></table></figure><p>排查思路：</p><ol><li>检查 Jenkins Pod Events（<code>kubectl describe pod &lt;jenkins-agent-pod&gt;</code>）</li><li>查看 Jenkins Plugin 的 GitHub Issues &#x2F; Changelog</li><li><strong>在非生产 Jenkins 实例先升级验证</strong>（500+ 规模的中断代价很高，升级必须有预演）</li></ol><h3 id="7-4-allowOverride-false-的边界情况"><a href="#7-4-allowOverride-false-的边界情况" class="headerlink" title="7.4 allowOverride: false 的边界情况"></a>7.4 <code>allowOverride: false</code> 的边界情况</h3><p>在 500 个仓库中，总会有团队尝试覆盖平台强制容器：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">containers:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">jnlp</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">my-custom-jenkins-agent:latest</span>  <span class="comment"># 试图替换 jnlp 容器</span></span><br></pre></td></tr></table></figure><p>如果 <code>ConfigMerger</code> 实现不当（先覆盖再检查 <code>allowOverride</code>），这类问题会导致 CI 行为不一致且难以复现。</p><p>正确实现：<strong>先检查</strong> <code>allowOverride</code>，再决定是否合并：</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (defaultContainer.allowOverride == <span class="literal">false</span>) &#123;</span><br><span class="line">    echo <span class="string">&quot;Skipping override for protected container: $&#123;repoContainer.name&#125;&quot;</span></span><br><span class="line">    <span class="keyword">return</span>  <span class="comment">// 直接跳过，不做任何合并</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同时输出明确的警告信息——在 500 个仓库中，平台团队无法逐一沟通，日志必须自解释。</p><h3 id="7-5-大规模并发时的-Vault-限流"><a href="#7-5-大规模并发时的-Vault-限流" class="headerlink" title="7.5 大规模并发时的 Vault 限流"></a>7.5 大规模并发时的 Vault 限流</h3><p>500+ 仓库同时触发 CI 时，AppRole 的 <code>/v1/auth/approle/login</code> 请求并发量可能达到每分钟数百次。Vault 有请求限流配置，超限后返回 429 错误，导致大量 Pipeline 的 Vault 认证步骤失败。</p><p>缓解措施：</p><ul><li>在 Vault 配置中提高 <code>max_request_duration</code> 和并发限制</li><li>在 <code>VaultClient.getToken()</code> 中加入指数退避重试逻辑</li><li>考虑缓存同一仓库短时间内的重复认证请求</li></ul><hr><h2 id="八、运行效果"><a href="#八、运行效果" class="headerlink" title="八、运行效果"></a>八、运行效果</h2><p>一个典型的业务仓库 CI 运行后，Jenkins UI 中显示的流水线结构：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">✅ Checkout</span><br><span class="line">✅ Config Parse</span><br><span class="line">✅ Lint (parallel)</span><br><span class="line">   ✅ PyLint</span><br><span class="line">   ✅ ShellCheck</span><br><span class="line">✅ Security Scan (parallel)</span><br><span class="line">   ✅ Semgrep</span><br><span class="line">   ✅ Dependency Audit</span><br><span class="line">✅ Unit Test</span><br><span class="line">✅ Build (仅 trunk/releases/latest 分支)</span><br><span class="line">✅ Deploy (仅 trunk 分支)</span><br><span class="line">⏭ Prepare Release (仅针对 release PR，此次跳过)</span><br></pre></td></tr></table></figure><p>业务团队看到的是：CI 通过了。他们不需要知道 Vault 在哪里、Registry 是哪个、Semgrep 规则集版本是什么。<strong>这种体验在第 1 个仓库和第 500 个仓库上完全相同</strong>——这正是统一平台的价值。</p><hr><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>Jenkins Shared Library 在 500+ 仓库规模下的核心工程价值：</p><ol><li><strong><code>default.yaml</code> + <code>allowOverride</code></strong>：明确区分”平台强制”和”业务可配置”，500 个仓库的合规基线通过数据驱动保证</li><li><strong>ConfigMerger</strong>：类型安全的配置合并，Groovy 的类型系统在复杂合并场景下比 shell + yq 更可靠</li><li><strong>StageGenerator</strong>：运行时动态决定流水线结构，天然支持 500 个仓库的差异化 stage 需求</li><li><strong>规模化运维挑战</strong>：主节点 OOM、Vault 限流、插件升级——这些在小规模时不显著的问题在 500+ 规模下需要系统性应对</li></ol><p>下一篇将介绍如何在 GitHub Actions 中实现等价能力，以及在 500+ 规模下 GitHub Actions 方案特有的工程优势。</p>]]>
    </content>
    <id>https://www.realks.com/2026/06/21/unified-cicd-02-jenkins/</id>
    <link href="https://www.realks.com/2026/06/21/unified-cicd-02-jenkins/"/>
    <published>2026-06-21T02:10:00.000Z</published>
    <summary>
      <![CDATA[<h1 id="Jenkins-Shared-Library：统一流水线的工程实现"><a href="#Jenkins-Shared-Library：统一流水线的工程实现" class="headerlink" title="Jenkins Shared Library：统一流水线的工程实现"></a>Jenkins Shared Library：统一流水线的工程实现</h1><blockquote>
<p>本文是《统一 CI&#x2F;CD 流水线治理》系列第二篇。上一篇讲了为什么要统一管理，这篇深入 Jenkins Shared Library 的技术实现细节。本文来自一个覆盖 500+ 仓库、运行 2 年以上的生产实践。</p>
</blockquote>
<hr>]]>
    </summary>
    <title>Jenkins Shared Library：统一流水线的工程实现</title>
    <updated>2026-06-21T02:10:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="DevOps" scheme="https://www.realks.com/categories/DevOps/"/>
    <category term="CI/CD" scheme="https://www.realks.com/categories/DevOps/CI-CD/"/>
    <category term="DevOps" scheme="https://www.realks.com/tags/DevOps/"/>
    <category term="Jenkins" scheme="https://www.realks.com/tags/Jenkins/"/>
    <category term="CI/CD" scheme="https://www.realks.com/tags/CI-CD/"/>
    <category term="Platform Engineering" scheme="https://www.realks.com/tags/Platform-Engineering/"/>
    <category term="GitHub Actions" scheme="https://www.realks.com/tags/GitHub-Actions/"/>
    <content>
      <![CDATA[<h1 id="为什么要统一管理-CI-CD-流水线：分散的真实代价与治理价值"><a href="#为什么要统一管理-CI-CD-流水线：分散的真实代价与治理价值" class="headerlink" title="为什么要统一管理 CI&#x2F;CD 流水线：分散的真实代价与治理价值"></a>为什么要统一管理 CI&#x2F;CD 流水线：分散的真实代价与治理价值</h1><blockquote><p>当你的组织有 500 个仓库各自维护一套 CI&#x2F;CD 配置时，问题不是”要不要统一”，而是”分散的代价你能承受多久”。本文来自一个已在 500+ 仓库生产环境运行 2 年以上的统一 CI&#x2F;CD 平台的实践总结。</p></blockquote><hr><span id="more"></span><h2 id="一、一个真实的场景"><a href="#一、一个真实的场景" class="headerlink" title="一、一个真实的场景"></a>一、一个真实的场景</h2><p>平台团队在某个周五下午收到安全团队通知：代码扫描工具需要升级，旧版本存在已知漏洞，要求在 <strong>2 周内全部迁移</strong>。</p><p>如果是统一流水线，平台团队修改一个文件，提一个 PR，合并后所有仓库在下次 CI 运行时自动使用新版本。<strong>2 小时内完成，500 个仓库同步更新。</strong></p><p>如果是 500 个仓库各自维护，则需要：</p><ol><li>找出所有仓库的 CI 配置文件——格式各异，有 <code>Jenkinsfile</code>、<code>ci.yaml</code>、<code>build.groovy</code>、<code>.travis.yml</code></li><li>评估每个仓库的版本和升级影响（其中约 80 个已无活跃维护者）</li><li>为每个仓库提 PR，等待各自团队 review 和合并</li><li>协调 500 个团队，追踪哪些完成、哪些在等待、哪些无人响应</li><li>两周后复查，发现仍有 130+ 个仓库未完成</li><li>向安全团队解释”部分合规”的原因</li></ol><p>在 500+ 仓库规模下，这不是一个可执行的方案——这是一个系统性失控。</p><hr><h2 id="二、分散管理在-500-规模下的四类代价"><a href="#二、分散管理在-500-规模下的四类代价" class="headerlink" title="二、分散管理在 500+ 规模下的四类代价"></a>二、分散管理在 500+ 规模下的四类代价</h2><h3 id="2-1-安全策略变更传播的指数级成本"><a href="#2-1-安全策略变更传播的指数级成本" class="headerlink" title="2.1 安全策略变更传播的指数级成本"></a>2.1 安全策略变更传播的指数级成本</h3><p>安全相关的 CI 步骤包括：依赖漏洞扫描、静态代码分析（SAST）、容器镜像扫描、代码签名。这些工具会定期更新，扫描规则会调整，认证凭证会轮换。</p><p>在小规模（20 个仓库）时，人工协调勉强可行。到 500+ 仓库时，”广播问题”演变成一个不可解的协调问题：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">平台团队发出通知（第0天）</span><br><span class="line">    │</span><br><span class="line">    ├─► 仓库 001-100（核心团队，约2周内完成）</span><br><span class="line">    ├─► 仓库 101-250（普通团队，推迟到下个 Sprint，约4周）</span><br><span class="line">    ├─► 仓库 251-380（低活跃仓库，原维护者离职，无人响应）</span><br><span class="line">    ├─► 仓库 381-450（使用不同 CI 工具，通知没触达）</span><br><span class="line">    └─► 仓库 451-500（外包团队，响应周期不定）</span><br><span class="line"></span><br><span class="line">第 60 天：仍有约 200 个仓库未完成</span><br><span class="line">第 90 天：审计截止日，组织处于&quot;部分合规&quot;状态</span><br></pre></td></tr></table></figure><p>“部分合规”在审计中比”明确不合规”更棘手——你无法简洁地说明状态，只能提供一份不断变化的进度列表。</p><p><strong>统一管理的改变：</strong> 平台团队提交 1 个 PR → 合并 → <strong>500 个仓库在 24 小时内完成更新</strong>（随各仓库下次 CI 运行）。安全团队得到一个确定的答案。</p><h3 id="2-2-凭证泄漏面积：从-1-个入口到-500-个风险点"><a href="#2-2-凭证泄漏面积：从-1-个入口到-500-个风险点" class="headerlink" title="2.2 凭证泄漏面积：从 1 个入口到 500 个风险点"></a>2.2 凭证泄漏面积：从 1 个入口到 500 个风险点</h3><p>Vault token、Registry 密码、代码签名证书——这些凭证在 CI 中使用。分散管理时：</p><ul><li>500 个仓库的 Jenkinsfile 或 CI YAML 都可能错误引用凭证</li><li>凭证轮换时，需要确认 500 个地方都已更新（通常做不到）</li><li>某个开发者在调试时把临时 Registry 密码提交到 CI 配置里——在 500 个仓库中这每年至少发生数次</li></ul><p><strong>真实案例（已脱敏）：</strong> 平台团队在安全扫描中发现，有 23 个仓库的 CI 配置文件中存在凭证引用问题——有的是硬编码测试密码，有的是已失效但仍存在的 token 字符串。清理这 23 个问题耗时 3 周，因为每个仓库都需要单独联系负责人、确认影响、提 PR、合并。</p><p>在统一管理架构中，这不可能发生——<strong>业务仓库的 CI 文件没有任何凭证</strong>，凭证只存在于平台的 Vault 中，通过 JWT&#x2F;OIDC 运行时动态获取。</p><h3 id="2-3-合规审计：从”一句话”到”500-份文档的汇总”"><a href="#2-3-合规审计：从”一句话”到”500-份文档的汇总”" class="headerlink" title="2.3 合规审计：从”一句话”到”500 份文档的汇总”"></a>2.3 合规审计：从”一句话”到”500 份文档的汇总”</h3><p>SOC 2、ISO 27001 等合规框架要求证明所有代码在合并前经过了安全扫描。</p><p><strong>分散管理（500 个仓库）时，审计对话：</strong></p><ul><li>审计员：”你们所有仓库都启用了 SAST 扫描吗？”</li><li>平台团队：”应该都有，但我们需要逐一检查才能确认”</li><li>审计员（三周后）：”我们抽查了 30 个仓库，其中 8 个 SAST 配置不一致或版本过旧”</li><li>结果：审计发现，需要整改</li></ul><p><strong>统一管理时，审计对话：</strong></p><ul><li>审计员：”你们所有仓库都启用了 SAST 扫描吗？”</li><li>平台团队：”是的。所有 500+ 个仓库调用同一个 <code>platform-ci-core.yml</code> 入口，该入口强制执行安全扫描步骤（<code>allowOverride: false</code>），业务团队无法绕过。这是 Vault 审计日志，这是过去 30 天的 CI 运行记录。”</li><li>结果：审计通过，5 分钟内给出完整证据</li></ul><p>在 500+ 规模下，合规审计的答案质量直接影响审计结论。统一管理让”所有仓库的安全基线一致”从一个努力方向变成一个可证明的事实。</p><h3 id="2-4-最佳实践漂移：500-个仓库，500-个不同时间点的”最佳实践”"><a href="#2-4-最佳实践漂移：500-个仓库，500-个不同时间点的”最佳实践”" class="headerlink" title="2.4 最佳实践漂移：500 个仓库，500 个不同时间点的”最佳实践”"></a>2.4 最佳实践漂移：500 个仓库，500 个不同时间点的”最佳实践”</h3><p>CI&#x2F;CD 最佳实践持续演进：缓存策略从无到有、并行度从单线程到并行、构建缓存从本地到分布式、artifact 管理从临时存储到带保留策略的管理。</p><p>分散管理时，每个仓库的 CI 配置质量取决于它最后一次被认真维护的时间点。在 500 个仓库中：</p><ul><li>约 100 个仓库有积极维护，CI 质量较好</li><li>约 200 个仓库是 2-3 年前写的配置，用着当年的”最佳实践”</li><li>约 150 个仓库是从其他仓库 copy-paste 来的，连原始的注释都没有改</li><li>约 50 个仓库几乎无人知道 CI 是怎么跑起来的</li></ul><p><strong>结果：</strong> 组织的 CI 速度和质量呈现严重的长尾分布。某些仓库的 CI 需要 45 分钟，另一些只需要 8 分钟——不是因为业务复杂度不同，而是因为 CI 配置质量天壤之别。</p><p>统一管理后，平台团队对所有仓库做一次构建速度优化，500 个仓库同步受益。这是规模效应的直接体现。</p><hr><h2 id="三、统一管理的核心价值：分离关注点"><a href="#三、统一管理的核心价值：分离关注点" class="headerlink" title="三、统一管理的核心价值：分离关注点"></a>三、统一管理的核心价值：分离关注点</h2><p>统一管理的本质不是”控制”，而是<strong>明确划定谁负责什么</strong>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">平台团队负责（一次实现，500+ 仓库受益）：</span><br><span class="line">├─ CI/CD 流水线逻辑（如何 build、如何 test、如何 deploy）</span><br><span class="line">├─ 安全工具的版本和配置</span><br><span class="line">├─ 凭证管理和轮换（Vault JWT/OIDC）</span><br><span class="line">├─ 合规规则的执行（allowOverride: false 强制步骤）</span><br><span class="line">├─ 流水线可观测性（日志、通知、状态上报）</span><br><span class="line">└─ 性能优化（构建缓存、并行度调整）</span><br><span class="line"></span><br><span class="line">业务团队负责（只需维护这一件事）：</span><br><span class="line">├─ 业务代码</span><br><span class="line">├─ 测试代码</span><br><span class="line">├─ .ci-config/config.yaml（声明业务意图）</span><br><span class="line">└─ 接受流水线运行结果</span><br></pre></td></tr></table></figure><p>在 500+ 规模下，这个分离的价值是<strong>线性叠加</strong>的：平台团队的每一次改进，都乘以 500 这个系数。</p><p>业务团队的 <code>.ci-config/config.yaml</code> 是<strong>意图声明</strong>，不是实现细节：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 业务团队的 .ci-config/config.yaml</span></span><br><span class="line"><span class="comment"># 我需要 Python 3.12 环境</span></span><br><span class="line"><span class="attr">containers:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">project-runtime</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">platform-registry.example.com/python:3.12</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 我需要对这几个模块做 lint</span></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">lint</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">pyLint:</span></span><br><span class="line">          <span class="attr">sourceSets:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">src/my_module</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">src/another_module</span></span><br><span class="line">          <span class="attr">rcFile:</span> <span class="string">.pylintrc</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 我有单元测试</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">unit-test</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">script:</span></span><br><span class="line">          <span class="attr">workspace:</span> <span class="string">tests/run_tests.sh</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 我需要构建容器镜像（公网发布）</span></span><br><span class="line"><span class="attr">containerBuild:</span></span><br><span class="line">  <span class="attr">path:</span> <span class="string">Dockerfile</span></span><br><span class="line">  <span class="attr">registryType:</span> <span class="string">internet</span></span><br></pre></td></tr></table></figure><p>业务团队<strong>不需要知道</strong>：</p><ul><li>扫描工具的版本（Semgrep 的规则集版本由平台统一管理）</li><li>镜像 Registry 的地址（随环境自动路由）</li><li>Vault 凭证的路径（JWT&#x2F;OIDC 运行时获取）</li><li>如何打镜像 tag、如何签名、如何上报状态</li></ul><p>这些由平台团队在流水线代码中处理，<strong>一次修改，500 个仓库同步受益</strong>。</p><hr><h2 id="四、业务团队的接入体验"><a href="#四、业务团队的接入体验" class="headerlink" title="四、业务团队的接入体验"></a>四、业务团队的接入体验</h2><p>理想状态下，一个新业务仓库接入统一 CI 的完整工作是：</p><ol><li>创建 <code>.ci-config/config.yaml</code>，声明运行环境和所需任务（约 20 行 YAML）</li><li>在 <code>.github/workflows/ci.yml</code> 中调用平台流水线（约 15 行 YAML）</li><li>提交代码，流水线自动运行</li></ol><p><strong>没有</strong> Vault 配置，<strong>没有</strong> Registry 凭证，<strong>没有</strong> 工具版本选择。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 业务仓库的 .github/workflows/ci.yml（完整文件）</span></span><br><span class="line"><span class="attr">name:</span> <span class="string">CI</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">trunk</span>, <span class="string">releases/latest</span>]</span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">trunk</span>, <span class="string">releases/latest</span>]</span><br><span class="line">  <span class="attr">pull_request_target:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">trunk</span>, <span class="string">releases/latest</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">pipeline:</span></span><br><span class="line">    <span class="attr">if:</span> <span class="string">&gt;-</span></span><br><span class="line"><span class="string">      github.event_name != &#x27;pull_request_target&#x27; ||</span></span><br><span class="line"><span class="string">      github.event.pull_request.head.repo.full_name != github.repository</span></span><br><span class="line"><span class="string"></span>    <span class="attr">uses:</span> <span class="string">OrgA/.github/.github/workflows/platform-ci-core.yml@main</span></span><br><span class="line">    <span class="comment"># 无 with: 块 —— 所有配置从 .ci-config/config.yaml 读取</span></span><br><span class="line">    <span class="comment"># 无 secrets: 块 —— 凭证由平台流水线内部处理</span></span><br></pre></td></tr></table></figure><p>这 15 行是业务团队需要维护的全部 CI 代码。在 500+ 仓库规模下，接入流程的简洁程度直接决定了新团队的上手速度和存量仓库的迁移成本。</p><hr><h2 id="五、500-规模特有的挑战"><a href="#五、500-规模特有的挑战" class="headerlink" title="五、500+ 规模特有的挑战"></a>五、500+ 规模特有的挑战</h2><p>在小规模（20-50 个仓库）时不显著的问题，在 500+ 规模时会成为系统性痛点：</p><h3 id="5-1-Onboarding-自动化"><a href="#5-1-Onboarding-自动化" class="headerlink" title="5.1 Onboarding 自动化"></a>5.1 Onboarding 自动化</h3><p>手动接入 20 个仓库是可行的，接入 500 个需要工具化：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 批量检查哪些仓库还没有 .ci-config/config.yaml</span></span><br><span class="line">gh repo list OrgA --<span class="built_in">limit</span> 1000 --json name,defaultBranchRef \</span><br><span class="line">  | jq -r <span class="string">&#x27;.[].name&#x27;</span> \</span><br><span class="line">  | <span class="keyword">while</span> <span class="built_in">read</span> repo; <span class="keyword">do</span></span><br><span class="line">      <span class="keyword">if</span> ! gh api <span class="string">&quot;repos/OrgA/<span class="variable">$&#123;repo&#125;</span>/contents/.ci-config/config.yaml&quot;</span> &amp;&gt;/dev/null; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$&#123;repo&#125;</span>: 缺少 .ci-config/config.yaml&quot;</span></span><br><span class="line">      <span class="keyword">fi</span></span><br><span class="line">    <span class="keyword">done</span></span><br></pre></td></tr></table></figure><p>平台团队需要提供脚手架工具，让新仓库可以用一条命令生成标准的 <code>config.yaml</code> 模板。</p><h3 id="5-2-配置漂移检测"><a href="#5-2-配置漂移检测" class="headerlink" title="5.2 配置漂移检测"></a>5.2 配置漂移检测</h3><p>500 个仓库接入后，随着时间推移，部分仓库的 <code>config.yaml</code> 可能变得不合规（字段格式变更、废弃字段未清理、新增必填字段缺失）。需要定期的合规性扫描：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 定期扫描所有仓库的 .ci-config/config.yaml 合规性</span></span><br><span class="line"><span class="comment"># 检查必填字段、检查废弃字段、检查版本兼容性</span></span><br><span class="line"><span class="keyword">for</span> repo <span class="keyword">in</span> $(get_all_repos); <span class="keyword">do</span></span><br><span class="line">  validate_config <span class="string">&quot;<span class="variable">$&#123;repo&#125;</span>/.ci-config/config.yaml&quot;</span> || \</span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;<span class="variable">$&#123;repo&#125;</span>: 配置不合规&quot;</span> &gt;&gt; violation_report.txt</span><br><span class="line"><span class="keyword">done</span></span><br></pre></td></tr></table></figure><h3 id="5-3-可观测性：500-个流水线同时运行"><a href="#5-3-可观测性：500-个流水线同时运行" class="headerlink" title="5.3 可观测性：500 个流水线同时运行"></a>5.3 可观测性：500 个流水线同时运行</h3><p>在 500+ 规模下，平台团队需要知道：</p><ul><li>今天有多少个 CI 运行失败？失败原因分布？</li><li>平均 CI 耗时趋势？哪些仓库是异常慢的尾部？</li><li>安全扫描覆盖率？哪些仓库超过 30 天没有 CI 运行？</li></ul><p>这需要在流水线中内置 metrics 上报，以及统一的 dashboard。</p><h3 id="5-4-Runner-容量规划"><a href="#5-4-Runner-容量规划" class="headerlink" title="5.4 Runner 容量规划"></a>5.4 Runner 容量规划</h3><p>500+ 仓库同时触发 CI（如 trunk push 后）会产生并发 job 峰值。需要根据历史数据规划 runner 数量和自动扩缩容策略。</p><hr><h2 id="六、”统一”不等于”强制相同”"><a href="#六、”统一”不等于”强制相同”" class="headerlink" title="六、”统一”不等于”强制相同”"></a>六、”统一”不等于”强制相同”</h2><p>统一管理容易被误解为”所有仓库必须用完全相同的 CI”。好的统一管理架构支持<strong>受控的差异化</strong>：</p><ul><li>有些仓库有单元测试，有些没有 → <code>config.yaml</code> 声明是否有 <code>unit-test</code> job</li><li>有些仓库需要构建容器镜像，有些是纯 Python 库 → <code>config.yaml</code> 声明是否有 <code>containerBuild</code></li><li>有些仓库镜像需要公网发布 → <code>registryType: internet</code></li><li>lint 的 rcFile 可以是仓库自己的 → <code>rcFile: .pylintrc</code></li></ul><p>平台团队通过 <code>allowOverride</code> 机制明确哪些是可定制的，哪些是平台强制的：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># platform-defaults.yaml（平台团队维护）</span></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">security-scan</span></span><br><span class="line">    <span class="attr">allowOverride:</span> <span class="literal">false</span>  <span class="comment"># 安全扫描不允许业务团队关闭</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">semgrep:</span></span><br><span class="line">          <span class="attr">rulesets:</span> [<span class="string">&quot;p/python&quot;</span>]</span><br><span class="line"></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">lint</span></span><br><span class="line">    <span class="attr">allowOverride:</span> <span class="literal">true</span>   <span class="comment"># lint 配置允许业务团队覆盖</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">pyLint:</span></span><br><span class="line">          <span class="attr">sourceSets:</span> []</span><br></pre></td></tr></table></figure><p>即使某个业务团队在 <code>.ci-config/config.yaml</code> 中写了 <code>security-scan: disabled</code>，流水线也会忽略并强制运行安全扫描。这是 500+ 仓库合规基线的技术保证。</p><hr><h2 id="七、投入与回报的规模效应"><a href="#七、投入与回报的规模效应" class="headerlink" title="七、投入与回报的规模效应"></a>七、投入与回报的规模效应</h2><p>统一管理的初始投入是固定的（平台团队设计和实现流水线框架），但收益随仓库数量线性增长：</p><table><thead><tr><th>规模</th><th>安全更新传播时间</th><th>凭证泄漏风险点</th><th>合规审计准备时间</th></tr></thead><tbody><tr><td>分散管理，500 仓库</td><td>2-3 个月（仍有遗漏）</td><td>500 个</td><td>数周（可能发现不合规）</td></tr><tr><td>统一管理，500 仓库</td><td>&lt; 24 小时</td><td>1 个（平台入口）</td><td>&lt; 1 小时</td></tr><tr><td>规模效应</td><td><strong>60-90x 提升</strong></td><td><strong>500x 缩减</strong></td><td><strong>数十倍提升</strong></td></tr></tbody></table><p>这个数字在 20 个仓库时看起来”够用”，在 500 个仓库时变成了战略差距。</p><hr><h2 id="八、小结"><a href="#八、小结" class="headerlink" title="八、小结"></a>八、小结</h2><p>分散 CI 管理在 500+ 规模下的代价是系统性的：</p><ul><li><strong>安全更新传播</strong>：从”2 小时”变成”3 个月且仍未完成”</li><li><strong>凭证泄漏面积</strong>：从”1 个入口”变成”500 个潜在风险点”</li><li><strong>合规审计</strong>：从”5 分钟给出证据”变成”数周整改”</li><li><strong>最佳实践漂移</strong>：500 个仓库，500 个不同时代的 CI 配置</li></ul><p>统一管理的核心价值是<strong>分离关注点</strong>，结合规模效应：平台团队掌控流水线实现，业务团队只需声明意图，每一次平台改进都乘以 500 的系数。<code>.ci-config/config.yaml</code> 是这个边界的物理载体。</p><p>本系列后续三篇将分别介绍：</p><ul><li><a href="/2026/06/21/unified-cicd-02-jenkins/">Jenkins Shared Library 的完整工程实现</a></li><li><a href="/2026/06/21/unified-cicd-03-github-actions/">GitHub Actions Reusable Workflow 的零配置架构</a></li><li><a href="/2026/06/21/unified-cicd-04-migration/">从 Jenkins 迁移到 GitHub Actions 的决策框架</a></li></ul>]]>
    </content>
    <id>https://www.realks.com/2026/06/21/unified-cicd-01-why/</id>
    <link href="https://www.realks.com/2026/06/21/unified-cicd-01-why/"/>
    <published>2026-06-21T02:00:00.000Z</published>
    <summary>
      <![CDATA[<h1 id="为什么要统一管理-CI-CD-流水线：分散的真实代价与治理价值"><a href="#为什么要统一管理-CI-CD-流水线：分散的真实代价与治理价值" class="headerlink" title="为什么要统一管理 CI&#x2F;CD 流水线：分散的真实代价与治理价值"></a>为什么要统一管理 CI&#x2F;CD 流水线：分散的真实代价与治理价值</h1><blockquote>
<p>当你的组织有 500 个仓库各自维护一套 CI&#x2F;CD 配置时，问题不是”要不要统一”，而是”分散的代价你能承受多久”。本文来自一个已在 500+ 仓库生产环境运行 2 年以上的统一 CI&#x2F;CD 平台的实践总结。</p>
</blockquote>
<hr>]]>
    </summary>
    <title>为什么要统一管理 CI/CD 流水线：分散的真实代价与治理价值</title>
    <updated>2026-06-21T02:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="Cloud Computing" scheme="https://www.realks.com/categories/Cloud-Computing/"/>
    <category term="Homelab" scheme="https://www.realks.com/categories/Homelab/"/>
    <category term="Public Cloud Provider" scheme="https://www.realks.com/categories/Cloud-Computing/Public-Cloud-Provider/"/>
    <category term="Azure" scheme="https://www.realks.com/categories/Cloud-Computing/Public-Cloud-Provider/Azure/"/>
    <category term="Tencent Cloud" scheme="https://www.realks.com/categories/Cloud-Computing/Public-Cloud-Provider/Tencent-Cloud/"/>
    <category term="Homelab" scheme="https://www.realks.com/tags/Homelab/"/>
    <category term="Azure Active Directory" scheme="https://www.realks.com/tags/Azure-Active-Directory/"/>
    <category term="Tencent Cloud" scheme="https://www.realks.com/tags/Tencent-Cloud/"/>
    <content>
      <![CDATA[<p>在之前的博客《利用Azure AD实现Homelab环境中应用的统一认证和授权》中，我们详细讨论了如何使用Azure AD来实现统一认证。而在《Jenkins集成Azure AD》中，我们详细介绍了自托管的Jenkins如何与Azure AD集成。</p><p>在本文，我将介绍腾讯云如何和Azure AD集成。</p><span id="more"></span><h2 id="第一步：在Azure中创建应用"><a href="#第一步：在Azure中创建应用" class="headerlink" title="第一步：在Azure中创建应用"></a>第一步：在Azure中创建应用</h2><h3 id="1-在企业应用中创建一个新的应用"><a href="#1-在企业应用中创建一个新的应用" class="headerlink" title="1. 在企业应用中创建一个新的应用"></a>1. 在企业应用中创建一个新的应用</h3><p>在Azure Active Directory中找到Enterprise applications(企业应用)创建一个新的应用，参考下图，在本文中将会使用Tencent Cloud SSO作为应用名称。</p><img src="azure-ad-01.png" width="80%"><h3 id="2-配置SSO"><a href="#2-配置SSO" class="headerlink" title="2. 配置SSO"></a>2. 配置SSO</h3><p>完成创建之后，进入应用之后，按照下图创建基于SAML的SSO。</p><img src="azure-ad-02.png" width="80%"><p>需要配置的内容下图，总共五部分<br><img src="azure-ad-03.png" width="80%"></p><p>我们需要配置的主要是前两个部分。</p><h4 id="基础配置"><a href="#基础配置" class="headerlink" title="基础配置"></a>基础配置</h4><p>参考下图进行设置，标识符（实体 ID）和回复 URL（断言使用者服务 URL）参考下表<br><img src="azure-ad-04.png" width="80%"></p><table><thead><tr><th align="left">所在站点</th><th align="left">标识符（实体 ID）</th><th align="left">回复 URL（断言使用者服务 URL）</th></tr></thead><tbody><tr><td align="left">中国站</td><td align="left">cloud.tencent.com</td><td align="left"><a href="https://cloud.tencent.com/login/saml">https://cloud.tencent.com/login/saml</a></td></tr><tr><td align="left">国际站</td><td align="left">intl.cloud.tencent.com</td><td align="left"><a href="https://intl.cloud.tencent.com/login/saml">https://intl.cloud.tencent.com/login/saml</a></td></tr></tbody></table><h4 id="属性和声明"><a href="#属性和声明" class="headerlink" title="属性和声明"></a>属性和声明</h4><p>在配置中，主要增加了下图中标出的两条配置。</p><img src="azure-ad-05.png" width="80%"><table><thead><tr><th align="left">Name(名称)</th><th align="left">Namespace(命名空间)</th><th align="left">Source(来源)</th><th align="left">Source attribute(来源属性)</th></tr></thead><tbody><tr><td align="left">Role</td><td align="left"><a href="https://cloud.tencent.com/SAML/Attributes">https://cloud.tencent.com/SAML/Attributes</a></td><td align="left">Attribute</td><td align="left">user.assignedroles</td></tr><tr><td align="left">RoleSessionName</td><td align="left"><a href="https://cloud.tencent.com/SAML/Attributes">https://cloud.tencent.com/SAML/Attributes</a></td><td align="left">Attribute</td><td align="left">user.userprincipalname</td></tr></tbody></table><p>添加过程如下：</p><img src="azure-ad-06.png" width="80%"><img src="azure-ad-07.png" width="80%"><img src="azure-ad-08.png" width="80%"><h4 id="下载元数据文件"><a href="#下载元数据文件" class="headerlink" title="下载元数据文件"></a>下载元数据文件</h4><p>在第三部分中下载元数据文件。<br><img src="azure-ad-09.png" width="80%"></p><h2 id="第二步：在腾讯云中创建角色SSO"><a href="#第二步：在腾讯云中创建角色SSO" class="headerlink" title="第二步：在腾讯云中创建角色SSO"></a>第二步：在腾讯云中创建角色SSO</h2><h3 id="1-创建角色SSO"><a href="#1-创建角色SSO" class="headerlink" title="1. 创建角色SSO"></a>1. 创建角色SSO</h3><p>在访问管理-&gt;身份提供商-&gt;角色SSO中新建一个提供商。</p><img src="tencent-01.png" width="80%">参考下图创建一个新的提供商， 其中元数据文档为Azure AD中下载的元数据文件。<img src="tencent-02.png" width="80%"><p>打开创建完成的提供商，将登录链接保存下来。</p><h3 id="2-创建角色"><a href="#2-创建角色" class="headerlink" title="2. 创建角色"></a>2. 创建角色</h3><p>在访问管理-&gt;角色中新建角色，可以根据需求创建一个或者多个角色。在本文中，创建了两个角色，具备管理员权限的Administrator和只读权限的ReadOnly。</p><p>创建角色的过程如下：<br>点击“新建角色”后，在弹出框中选择“身份提供商”<br><img src="tencent-03.png" width="80%"><br>身份提供商类型选择SAML，身份提供商选择之前创建的身份提供商，在本文中为aad<br><img src="tencent-04.png" width="80%"><br>角色策略可以根据角色需求设置，比如现在创建的是Administrator，因此选中了AdministratorAccess<br><img src="tencent-05.png" width="80%"><br>然后下一步配置角色标签，根据需求自行添加。在审阅中设置角色名称。<br><img src="tencent-06.png" width="80%"><br>完成之后会自动跳转会角色列表，在角色列表中找到刚刚创建的角色并打开。参考下图找到两个重要信息：RoleArn和ProviderArn<br><img src="tencent-07.png" width="80%"></p><h2 id="第三步：在Azure-AD中添加角色"><a href="#第三步：在Azure-AD中添加角色" class="headerlink" title="第三步：在Azure AD中添加角色"></a>第三步：在Azure AD中添加角色</h2><h3 id="添加角色"><a href="#添加角色" class="headerlink" title="添加角色"></a>添加角色</h3><p>返回Azure Portal页面，在在Azure Active Directory -&gt; App registrations（应用注册）中选择All Applications，然后找到与企业应用同名的应用并打开，如下图。</p><img src="azure-ad-10.png" width="80%"><p>在Manifest中修改<code>appRoles</code>, 添加新的Role如下图所示。<br><img src="azure-ad-11.png" width="80%"></p><p>内容可参考如下设置</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;allowedMemberTypes&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="string">&quot;User&quot;</span></span><br><span class="line">    <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Administrator&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;displayName&quot;</span><span class="punctuation">:</span> <span class="string">&quot;[Root]Administrator&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xxxx&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;isEnabled&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;lang&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">null</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;origin&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Application&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;value&quot;</span><span class="punctuation">:</span> <span class="string">&quot;qcs::cam::uin/xxxxx:roleName/Administrator,qcs::cam::uin/xxxxx:saml-provider/aad&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>需要注意的是：</p><ul><li>id是uuid，可以自行生成</li><li>value的格式是<code>RoleArn,ProviderArn</code></li></ul><h3 id="给AD用户-用户组分配角色"><a href="#给AD用户-用户组分配角色" class="headerlink" title="给AD用户&#x2F;用户组分配角色"></a>给AD用户&#x2F;用户组分配角色</h3><p>返回到创建的企业应用中，找到“用户和组”，根据需求添加用户和组即可。<br><img src="azure-ad-12.png" width="80%"></p><h2 id="第四步：测试"><a href="#第四步：测试" class="headerlink" title="第四步：测试"></a>第四步：测试</h2><p>打开角色SSO中的登录链接，得到如下界面。</p><img src="tencent-08.png" width="80%">]]>
    </content>
    <id>https://www.realks.com/2023/03/04/tencent-cloud-azure-ad-sso/</id>
    <link href="https://www.realks.com/2023/03/04/tencent-cloud-azure-ad-sso/"/>
    <published>2023-03-04T12:43:36.000Z</published>
    <summary>
      <![CDATA[<p>在之前的博客《利用Azure AD实现Homelab环境中应用的统一认证和授权》中，我们详细讨论了如何使用Azure AD来实现统一认证。而在《Jenkins集成Azure AD》中，我们详细介绍了自托管的Jenkins如何与Azure AD集成。</p>
<p>在本文，我将介绍腾讯云如何和Azure AD集成。</p>]]>
    </summary>
    <title>腾讯云集成Azure AD实现多角色SSO</title>
    <updated>2023-03-04T12:43:36.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="DevOps" scheme="https://www.realks.com/categories/DevOps/"/>
    <category term="Cloud Computing" scheme="https://www.realks.com/categories/Cloud-Computing/"/>
    <category term="Homelab" scheme="https://www.realks.com/categories/Homelab/"/>
    <category term="Public Cloud Provider" scheme="https://www.realks.com/categories/Cloud-Computing/Public-Cloud-Provider/"/>
    <category term="Azure" scheme="https://www.realks.com/categories/Cloud-Computing/Public-Cloud-Provider/Azure/"/>
    <category term="Jenkins" scheme="https://www.realks.com/categories/DevOps/Jenkins/"/>
    <category term="Homelab" scheme="https://www.realks.com/tags/Homelab/"/>
    <category term="Azure Active Directory" scheme="https://www.realks.com/tags/Azure-Active-Directory/"/>
    <category term="Jenkins" scheme="https://www.realks.com/tags/Jenkins/"/>
    <content>
      <![CDATA[<p>在<a href="/2022/07/13/homelab-unified-authorization-authentication-1/">《利用Azure AD实现Homelab环境中应用的统一认证和授权》</a>中，我介绍了当前我是如何实现统一认证和授权。这一篇博客中，我将介绍Jenkins如何和Azure AD集成。</p><span id="more"></span><p>Jenkins等这一类的应用，我会选在部署在k8s集群上，主要原因如下：</p><ul><li>减少维护成本。比如升级Jenkins，我只需要修改helm，然后部署就行。</li><li>Agent可以在k8s集群中自动伸缩，不需要我再次配置。</li></ul><h2 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h2><p>为了保持环境干净，我会重新创建一个 <code>namespace</code>，并把Jenkins部署到该<code>namespace</code>之中。</p><p>使用如下命令创建 <code>namespace</code>，</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubectl create ns jenkins-lab</span><br></pre></td></tr></table></figure><p><strong>注意：</strong> 我使用了命令式命令（Imperative commands）创建 <code>namespace</code>, 这种方式不推荐在生产环境使用。具体参考<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/object-management/">Kubernetes Object Management</a>.</p><p>推荐使用helm部署Jenkins，Jenkins官方也提供了chart。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">helm repo add jenkins https://charts.jenkins.io</span><br><span class="line">helm upgrade -i jenkins jenkins/jenkins \</span><br><span class="line">  --namespace jenkins-lab \</span><br><span class="line">  --values values.yaml \</span><br><span class="line">  --timeout 10m --wait</span><br></pre></td></tr></table></figure><p>需要提供自定义一下<code>values</code>文件，这次部署过程中，我会使用如下配置：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">controller:</span><br><span class="line">  installLatestSpecifiedPlugins: true</span><br><span class="line">  overwritePlugins: true</span><br><span class="line">  installPlugins:</span><br><span class="line">    - kubernetes</span><br><span class="line">    - workflow-aggregator</span><br><span class="line">    - git</span><br><span class="line">    - configuration-as-code</span><br><span class="line">  additionalPlugins:</span><br><span class="line">    - azure-ad</span><br><span class="line">  ingress:</span><br><span class="line">    enabled: true</span><br><span class="line">    annotations:</span><br><span class="line">      cert-manager.io/cluster-issuer: route53</span><br><span class="line">    hostName: jenkins-lab.xxxx.com</span><br><span class="line">    tls:</span><br><span class="line">     - hosts:</span><br><span class="line">       - jenkins-lab.xxxx.com</span><br><span class="line">       secretName: jenkins-lab-xxx-com</span><br><span class="line">  prometheus:</span><br><span class="line">    enabled: true</span><br><span class="line">  size: &quot;20Gi&quot;</span><br></pre></td></tr></table></figure><p>需要注意的是，因为AAD App的reply url需要安全的通信方式，此处即Https。我配置了一个带用tls的ingress，之后会写一篇博客介绍如何自动申请SSL证书。</p><p>如果你需要更多的自定义配置，可以参考<a href="https://github.com/jenkinsci/helm-charts/blob/main/charts/jenkins/values.yaml">Jenkins Chart默认的values.yaml</a>。</p><p>到这里Jenkins部署完成，下一步注册AAD应用。</p><h2 id="注册Azure-AD应用"><a href="#注册Azure-AD应用" class="headerlink" title="注册Azure AD应用"></a>注册Azure AD应用</h2><p>注册Azure AD应用可以使用如下方式：</p><ul><li>通过<a href="https://aad.portal.azure.com/">Azure AD Portal</a>或者<a href="https://portal.azure.com/">Azure Portal</a>注册。</li><li>通过Azure CLI注册。</li></ul><p>通过web页面注册，交互比较容易，但是过程很难自动化，也没啥意思。因此我将采用CLI的方式，之后可以将其自动化。</p><p>注册一个名为 <code>jenkins-lab</code>的AAD应用</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">appName=&quot;jenkins-lab&quot;</span><br><span class="line">az ad app create --display-name $appName \</span><br><span class="line">  --sign-in-audience AzureADMyOrg \</span><br><span class="line">  --enable-access-token-issuance true \</span><br><span class="line">  --enable-id-token-issuance true \</span><br><span class="line">  --web-home-page-url https://jenkins-lab.xxxx.com \</span><br><span class="line">  --web-redirect-uris https://jenkins-lab.xxxx.com/securityRealm/finishLogin</span><br></pre></td></tr></table></figure><p><strong>此处有坑：</strong> 查询Azure CLI的官网和使用<code>azure --help</code>的结果可能有较大区别。建议直接使用 <code>--help</code>获取帮助信息。</p><p>因为要使用的Group进行权限管理，因此需要将<code>groupMembershipClaims</code>改为<code>SecurityGroup</code>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">appId=$(az ad app list --display-name $appName | jq -r -c &quot;.[0].appId&quot;)</span><br><span class="line">az ad app update --id $appId --set groupMembershipClaims=SecurityGroup</span><br></pre></td></tr></table></figure><p>接下来，需要AAD应用授予<code>User.Read.All</code>, <code>Group.Read.All</code>, <code>People.Read.All</code>应用权限，使用如下命令：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">microsoftGraphAppId=$(az ad sp list --query &quot;[?appDisplayName==&#x27;Microsoft Graph&#x27;].appId&quot; --all | jq -r -c &quot;.[]&quot;)</span><br><span class="line">permissions=($(az ad sp show --id $microsoftGraphAppId | jq -r &#x27;[.appRoles[] | select(.value == (&quot;User.Read.All&quot;, &quot;Group.Read.All&quot;, &quot;People.Read.All&quot;)) | &quot;\(.id)=Role&quot; ] | join(&quot; &quot;)&#x27;))</span><br><span class="line">az ad app permission add --id $appId --api $microsoftGraphAppId --api-permissions $permissions</span><br><span class="line">az ad app permission admin-consent --id $appId</span><br></pre></td></tr></table></figure><h2 id="Jenkins集成AAD"><a href="#Jenkins集成AAD" class="headerlink" title="Jenkins集成AAD"></a>Jenkins集成AAD</h2><p>第一步： 登录Jenkins，默认用户名为<code>admin</code>，密码使用下面的命令查询：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubectl exec --namespace jenkins-lab -it svc/jenkins -c jenkins -- /bin/cat /run/secrets/chart-admin-password &amp;&amp; echo</span><br></pre></td></tr></table></figure><p>第二步： 进入<code>Manage Jenkins</code> -&gt; <code>Security</code> -&gt; <code>Configure Global Security</code></p><p>第三步： 执行下面的命令，获取Azure AD应用的密钥。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">az ad app credential reset --id $appId</span><br></pre></td></tr></table></figure><p>得到如下结果，</p><img src="aad-app-credential.png" width="80%"><p>该密钥有效时长为1年</p><p>第四步： 配置<code>Security Realm</code> 并保存</p><img src="jenkins-aad-config.png" width="80%"><p>Client ID：使用第三步返回的结果中的<code>appId</code></p><p>Client Secret：使用第三步返回的结果中的<code>password</code></p><p>Tenant: 使用第三步返回的结果中的<code>tenant</code></p><p>第五步： 保存之后，注销登录</p><p>第六步： 重新进入Jenkins，会自动跳转到Microsoft登录页面。</p><p>第七步： 配置权限，并保存</p><img src="jenkins-authorization.png" width="80%"><p>在<code>Start typing a name</code>处，输入用户名或者组名，然后添加，授予合适的权限。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在集成的过程中仍然有大量的手动工作，之后计划使用<code>JCasC</code>改进，为什么现在不用？因为之前尝试过，没有成功。</p>]]>
    </content>
    <id>https://www.realks.com/2022/07/23/jenkins-deployment-and-integrate-with-aad/</id>
    <link href="https://www.realks.com/2022/07/23/jenkins-deployment-and-integrate-with-aad/"/>
    <published>2022-07-23T15:07:54.000Z</published>
    <summary>
      <![CDATA[<p>在<a href="/2022/07/13/homelab-unified-authorization-authentication-1/">《利用Azure AD实现Homelab环境中应用的统一认证和授权》</a>中，我介绍了当前我是如何实现统一认证和授权。这一篇博客中，我将介绍Jenkins如何和Azure AD集成。</p>]]>
    </summary>
    <title>Jenkins 集成 Azure AD</title>
    <updated>2022-07-23T15:07:54.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="DevOps" scheme="https://www.realks.com/categories/DevOps/"/>
    <category term="Cloud Computing" scheme="https://www.realks.com/categories/Cloud-Computing/"/>
    <category term="Homelab" scheme="https://www.realks.com/categories/Homelab/"/>
    <category term="Public Cloud Provider" scheme="https://www.realks.com/categories/Cloud-Computing/Public-Cloud-Provider/"/>
    <category term="Azure" scheme="https://www.realks.com/categories/Cloud-Computing/Public-Cloud-Provider/Azure/"/>
    <category term="Homelab" scheme="https://www.realks.com/tags/Homelab/"/>
    <category term="Authorization" scheme="https://www.realks.com/tags/Authorization/"/>
    <category term="Authentication" scheme="https://www.realks.com/tags/Authentication/"/>
    <category term="SSO" scheme="https://www.realks.com/tags/SSO/"/>
    <category term="Azure" scheme="https://www.realks.com/tags/Azure/"/>
    <category term="Azure Active Directory" scheme="https://www.realks.com/tags/Azure-Active-Directory/"/>
    <content>
      <![CDATA[<p>在我的Homelab中搭建了很多服务，比如NAS, Jenkins, Gitlab, SonarQube, Grafana等，如果每一个应用都使用独立的认证授权，我将面对如下问题：</p><ul><li>需要设置多个密码。</li><li>如果设置定期更改策略，就意味着需要定期更改多个应用的用户密码。</li><li>当添加一个用户到我的homelab环境的时候，需要在多个应用中添加用户，过程比较繁琐。</li><li>当一个用户的角色发生改变时。需要在多个应用中进行更改。</li></ul><p>我采用的解决方案是：Azure AD + Windows Server AD</p><span id="more"></span><h2 id="Azure-Active-Directory"><a href="#Azure-Active-Directory" class="headerlink" title="Azure Active Directory"></a><strong><strong>Azure Active Directory</strong></strong></h2><p>Azure Active Directory，简称Azure AD或者AAD，是一种基于云的标识和访问管理服务。 此服务可帮助员工访问外部资源，例如 Microsoft 365、Azure 门户和数以千计的其他 SaaS 应用程序。</p><h3 id="功能"><a href="#功能" class="headerlink" title="功能"></a>功能</h3><ul><li>Azure Active Directory Free。跨 Azure、Microsoft 365 和许多常用 SaaS 应用程序提供用户和组管理、本地目录同步、基本报告、云用户的自助密码更改以及单一登录。</li><li>Azure Active Directory Premium P1。 除了免费版功能，P1 还允许混合用户访问本地资源和云资源。 它还支持高级管理，例如动态组、自助服务组管理、Microsoft Identity Manager 以及允许本地用户进行自助密码重置的云写回功能。</li><li>Azure Active Directory Premium P2**。** 除了免费版和 P1 版功能，P2 还提供 <a href="https://docs.microsoft.com/zh-cn/azure/active-directory/identity-protection/overview-identity-protection">Azure Active Directory 标识保护</a>，可帮助对应用和重要的公司数据提供基于风险的条件访问，以及提供 <a href="https://docs.microsoft.com/zh-cn/azure/active-directory/privileged-identity-management/pim-getting-started">Privileged Identity Management</a>以便发现、限制和监视管理员及其对资源的访问，并在需要时提供实时访问。</li></ul><p>该部分内容来自其技术文档，更多内容请访问<a href="https://docs.microsoft.com/zh-cn/azure/active-directory/fundamentals/active-directory-whatis">什么是 Azure Active Directory？</a></p><h3 id="如何获取"><a href="#如何获取" class="headerlink" title="如何获取"></a>如何获取</h3><p>获取Azure Active Directory的方法有两种：</p><p>方法一：注册Azure账号即可使用Azure Active Directory Free版本。该方法是最简单，也是长期可用的。但是部分功能无法使用，比如使用本地写回进行自助式密码重置&#x2F;更改&#x2F;解锁。</p><p>方法二：通过<strong><strong>Microsoft 365 Developer Program</strong></strong>获取Azure AD Premium P2。该方式可以解锁所有Azure AD功能，但是通过该方式申请到Microsoft 365 E5订阅有效期只有120天，之后微软会根据规则决定是否自动续期。目前，可以在网上找到如何自动续期的方案。</p><h2 id="Active-Directory-Domain-Service"><a href="#Active-Directory-Domain-Service" class="headerlink" title="Active Directory Domain Service"></a>Active Directory Domain Service</h2><p>Active Directory 存储有关网络上对象的信息，并使管理员和用户可以轻松查找和使用这些信息。 Active Directory 使用结构化数据存储作为目录信息的逻辑分层组织的基础。</p><h3 id="如何获取AD"><a href="#如何获取AD" class="headerlink" title="如何获取AD"></a>如何获取AD</h3><p>获取AD有两种方式：</p><p>方法一，使用Azure Active Directory Domain Services。对于个人用户而言，价格有点高，比如在East Asia一个月至少需要109美元。其好处也是不言而喻的，可靠性是有保障的。</p><p>方法二，在本地Windows Server上安装AD。与我而言，我会选择这种方案，因为其成本相对而言会低很多，可靠性的优先级并不是很高。</p><h3 id="Windows-AD-和Azure-AD之间的同步"><a href="#Windows-AD-和Azure-AD之间的同步" class="headerlink" title="Windows AD 和Azure AD之间的同步"></a>Windows AD 和Azure AD之间的同步</h3><p>Azure 提供了现成的工具，只需要在Windows Server上安装Azure AD Connect sync。详细内容可以参考：<a href="https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-sync-whatis">https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-sync-whatis</a></p><p>如果AAD的license是P1或者P2，可以开启密码回写，这样就可以通过微软提供的服务进行密码修改。可以参考： <a href="https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-password-hash-synchronization">https://docs.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-password-hash-synchronization</a></p><p>如果只是free license，还想通过web页面修改密码，需要借助Remote Desktop Services来实现，可以参考：<a href="https://www.devopsage.com/how-to-setup-web-page-to-change-users-password/">https://www.devopsage.com/how-to-setup-web-page-to-change-users-password/</a></p><h3 id="如何管理用户和用户组"><a href="#如何管理用户和用户组" class="headerlink" title="如何管理用户和用户组"></a>如何管理用户和用户组</h3><p>我采用的方案如下：</p><ul><li>在Windows Server AD上维护用户组，之后用户组会自动同步到AAD上。</li><li>在Windows Server AD上维护用户，之后用户组会自动同步到AAD上。</li><li>根据使用场景创建用户组。以Jenkins为例，我将用户分为两类：Admin和User，Admin可以管理Jenkins的系统配置，User只能使用Jenkins，因此我会建立两个用户组：JenkinsAdmin 和 JenkinsUser。使用AAD实现SSO，最终根据用户组分配权限。</li></ul><h2 id="利用Azure-AD实现应用SSO"><a href="#利用Azure-AD实现应用SSO" class="headerlink" title="利用Azure AD实现应用SSO"></a>利用Azure AD实现应用SSO</h2><p>Azure AD可以与很多种身份验证和同步协议集成。通过身份验证集成，只需对使用旧式身份验证方法的应用程序进行少量更改（或无需更改），即可使用 Azure AD 及其安全和管理功能。 利用同步集成，可以将用户和组数据同步到 Azure AD，然后使用用户 Azure AD 管理功能。 某些同步模式还支持自动预配。</p><p>支持的旧式身份验证方式：</p><ul><li>基于标头的身份验证（Header-based authentication）</li><li>LDAP身份验证（LDAP authentication）</li><li>OAuth 2.0身份验证（OAuth 2.0 authentication）</li><li>OIDC身份验证（OIDC authentication）</li><li>基于密码的SSO身份验证（Password based SSO authentication）</li><li>RADIUS 身份验证（RADIUS authentication）</li><li>远程桌面网关服务（Remote Desktop Gateway services）</li><li>Secure Shell (SSH)</li><li>SAML 身份认证（SAML authentication）</li><li>Windows身份认证（Windows Authentication - Kerberos Constrained Delegation）</li></ul><p>支持的同步模式</p><ul><li>目录同步：从本地 Active Directory 环境同步到 Azure AD</li><li>LDAP同步</li><li>SCIM同步</li></ul><p>更多详细的内容可以看<a href="https://docs.microsoft.com/zh-cn/azure/active-directory/fundamentals/auth-sync-overview">Azure Active Directory 身份验证和同步协议概述 - Microsoft Entra | Microsoft Docs</a>，毕竟是官方文档。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>用一张图总结一下。在我的Homelab环境中，AD之间的同步，以及用户如何使用Azure AD登录到Jenkins上。</p><img src="azure-ad-sso.png" width="80%"><p>接下来我会分享几篇实践性的博客，讲讲Azure AD与Synology NAS, Jenkins, Gitlab等的集成。</p>]]>
    </content>
    <id>https://www.realks.com/2022/07/13/homelab-unified-authorization-authentication-1/</id>
    <link href="https://www.realks.com/2022/07/13/homelab-unified-authorization-authentication-1/"/>
    <published>2022-07-13T13:07:19.000Z</published>
    <summary>
      <![CDATA[<p>在我的Homelab中搭建了很多服务，比如NAS, Jenkins, Gitlab, SonarQube, Grafana等，如果每一个应用都使用独立的认证授权，我将面对如下问题：</p>
<ul>
<li>需要设置多个密码。</li>
<li>如果设置定期更改策略，就意味着需要定期更改多个应用的用户密码。</li>
<li>当添加一个用户到我的homelab环境的时候，需要在多个应用中添加用户，过程比较繁琐。</li>
<li>当一个用户的角色发生改变时。需要在多个应用中进行更改。</li>
</ul>
<p>我采用的解决方案是：Azure AD + Windows Server AD</p>]]>
    </summary>
    <title>利用Azure AD实现Homelab环境中应用的统一认证和授权</title>
    <updated>2022-07-13T13:07:19.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="Development" scheme="https://www.realks.com/categories/Development/"/>
    <category term="Development" scheme="https://www.realks.com/tags/Development/"/>
    <category term="README" scheme="https://www.realks.com/tags/README/"/>
    <content>
      <![CDATA[<p>在代码项目根目录里，我们会经常看见README.md。README是什么？它是项目的自我介绍，类似于你的简历，都是用来销售自己的，让公司雇佣你，让别人采纳你的项目。</p><p>在本文中，我将尝试着为介绍一下如何写README。</p><span id="more"></span><p>写README可以按照5W1H(what, why, when, how, where, who)的思路去写，主要还是使用What, Why, How, Who。</p><h2 id="What"><a href="#What" class="headerlink" title="What"></a>What</h2><p><strong>What</strong> 主要是以下两方面：</p><ul><li>是什么？</li><li>能干什么？（提供了什么功能）</li></ul><p>比如在 <a href="https://github.com/kubernetes/kubernetes">Kubernetes</a>  的README中，</p><blockquote><p>Kubernetes, also known as K8s, is an open source system for managing containerized applications across multiple hosts. It provides basic mechanisms for deployment, maintenance, and scaling of applications.</p></blockquote><p>通过这段话，我们知道了如下信息：</p><ul><li>Kubernetes, also known as K8s, is an open source system for managing containerized applications across multiple hosts.（<strong>是什么</strong>）</li><li>It provides basic mechanisms for deployment, maintenance, and scaling of applications.（<strong>能干什么？</strong>）</li></ul><p>除了<strong>是什么</strong>和<strong>能干什么</strong>，还可能会有起源，License等</p><h2 id="Why"><a href="#Why" class="headerlink" title="Why"></a>Why</h2><p><strong>Why</strong> 主要是说明<strong>动机:</strong></p><ul><li>为什么做？</li><li>解决了什么问题？</li><li>带来了哪些好处（benefits）？</li></ul><p>比如在<a href="https://github.com/hashicorp/vault">Hashicorp Vault</a> 中，</p><blockquote><p>A modern system requires access to a multitude of secrets: database credentials, API keys for external services, credentials for service-oriented architecture communication, etc. Understanding who is accessing what secrets is already very difficult and platform-specific. Adding on key rolling, secure storage, and detailed audit logs is almost impossible without a custom solution. This is where Vault steps in.</p></blockquote><p>第一句话介绍了背景，第二句话说明在该背景之下，所面对的困难。最后一句告诉大家Vault这个工具就是解决这些问题的。</p><p>关于带来了哪些好处，这部分通常而言是和What中能干什么（功能）有重合的。</p><h2 id="Who"><a href="#Who" class="headerlink" title="Who"></a>Who</h2><p><strong>Who</strong> 有如下内容：</p><ul><li>谁维护该项目？</li><li>有哪些贡献者？</li><li>沟通方式。比如slack channel。</li></ul><p>关于<strong>谁维护该项目</strong>，在开源项目中会默认是社区或者repo的拥有者维护。但是在内部项目，随着人员的流动，还是很有必要注明该项目当前是谁&#x2F;哪个团队负责维护的。</p><p>有哪些贡献者，在开源项目中经常会在README中看见很多提交代码人的头像，这样可以激励大家去提交代码。</p><p>比如在<a href="https://github.com/vuejs/vue">Vue</a>中Contribution部分就做了这件事情。</p><p>沟通方式和下面的部分内容有一定的重复，提供一个或者多个平台，发布信息，让大家交流，提供反馈等。</p><h2 id="How"><a href="#How" class="headerlink" title="How"></a>How</h2><p>这部分重点关注两类人：使用者和开发者。</p><p>使用者，也就是用户。对于用户，我们需要告诉用户如下内容：</p><ul><li>使用的前置条件（Prerequisite）。安装哪些依赖，需要什么credentials等等。比如我写一个docker-compose文件用来在本地运行Jenkins，那么我的前置条件就是：docker和docker-compose已经安装在本地了。</li><li>如何安装？个人建议：提供一个脚本实现一键安装，这样可以降低用户使用门槛。</li><li>如何使用？</li><li>如何卸载？这部分内容是绝大数项目所缺少的，本来的我只是想尝试一下，结果我安装之后就无法卸载了，这也是一件很蛋疼的事情。</li><li>如何提供反馈？用户在使用过程中，出现了bug，用户在哪里反馈这个bug。通常情况下，用户可以提issue，可以不写。如果不是，应当写出来。</li></ul><p>这部分内容通常会集中在Documentation或者Getting Started这一类的标题之下。</p><p>比如在<a href="https://github.com/hashicorp/vault#documentation-getting-started-and-certification-exams">Hashicorp Vault</a> 中，Documentation, Getting Started, and Certification Exams就是用来告诉用户如何使用。</p><p>再比如在<a href="https://github.com/vuejs/vue">Vue</a>中，Documentation就是指向一些案例和文档，Issues就是告诉用户遇到问题如何创建一个issue。</p><p>开发者，可能对你项目感兴趣的人，想在你的基础上添加一些功能，或者发现了bug帮助你修复。我们需要提供给开发者Contributing Guide，包含如下内容：</p><ul><li>技术栈。可以考虑使用badge来标示关键依赖。badge可以参考<a href="https://shields.io/">https://shields.io/</a></li><li>Code of Conduct</li><li>CI&#x2F;CD 状态。建议使用badge。</li><li>如何执行测试？</li><li>如何执行代码风格检查?</li><li>如何构建（build）?</li><li>如何提交代码？如果涉及多分支，说明不同分支的用途。</li></ul><p>以上的每一项不一定都需要，以上的内容也不是全部内容。通常这部分内容会出现在Development，contribution等这一类的标题之下。</p><p>比如在<a href="https://github.com/hashicorp/vault#documentation-getting-started-and-certification-exams">Hashicorp Vault</a> 中，有一个小节Developing Vault来告诉开发者如何开发。</p><p>在<a href="https://github.com/vuejs/vue">Vue</a>中，并不是将如何贡献代码写到了contribution之下，而是将其内容放到了另外一个页面，这是非常好的做法。**在写README的时候，如果某个部分内容太长了就应该将其移到另外一个文件中。保持README完整，简洁，有条理是很重要的。**不要尝试着增加用户（使用者&#x2F;开发者）的阅读负担。</p>]]>
    </content>
    <id>https://www.realks.com/2022/06/28/how-to-write-readme/</id>
    <link href="https://www.realks.com/2022/06/28/how-to-write-readme/"/>
    <published>2022-06-28T13:00:00.000Z</published>
    <summary>
      <![CDATA[<p>在代码项目根目录里，我们会经常看见README.md。README是什么？它是项目的自我介绍，类似于你的简历，都是用来销售自己的，让公司雇佣你，让别人采纳你的项目。</p>
<p>在本文中，我将尝试着为介绍一下如何写README。</p>]]>
    </summary>
    <title>如何写README</title>
    <updated>2022-06-28T13:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="DevOps" scheme="https://www.realks.com/categories/DevOps/"/>
    <category term="Development" scheme="https://www.realks.com/categories/Development/"/>
    <category term="DevOps" scheme="https://www.realks.com/tags/DevOps/"/>
    <category term="Git" scheme="https://www.realks.com/tags/Git/"/>
    <category term="Version Control" scheme="https://www.realks.com/tags/Version-Control/"/>
    <content>
      <![CDATA[<p>版本控制记录着软件的每一次改变，每一次发布，以及每一个Bug。 它贯穿于软件的生命周期，从生到死，<strong>请慎重对待每一次提交，像记录历史一样书写提交记录</strong>。</p><p>当下最流行的Git是一个不错的选择，<strong>作为合格的软件开发人员你应该熟练的使用它</strong>。你可以构造一些场景去练习git命令，比如：</p><span id="more"></span><ul><li>在Github创建一个repo，并向其提交代码。（clone, add, commit, push）</li><li>从main分支创建一个新的分支dev，改变代码，然后通过PR的方式将代码合并到main分支。(checkout, pr, branch)</li><li>从main分支创建一个新的分支dev，改变代码提交。然后切回main分支，改变代码，提交至远端分支。再切换dev分支，将main分支的提交合并到当前分支。（练习rebase和reset）</li><li>从main分支创建一个新的分支dev, 创建7个commit，然后将他们合并为1个commit. （练习使用reset或者rebase）</li><li>从main分支创建一个新的分支dev, 创建7个commit，然后将第七个commit合并到第一个commit，抛弃第二个commit，仅修改第三个commit的commit message，仅修改第四个commit的commit author，修改第六个commit的代码。（练习rebase，以及如何整理commit message）</li><li>Fork一个repo，本地clone，修改代码。在被fork的repo主分支出现新的提交之后，将本地代码和被fork的repo同步。（练习remote, fetch, rebase）</li></ul><p>这里只列举了一些场景，你也可以根据你的需求再写一些场景用来练习。<strong>建议使用命令行练习</strong>，也建议以后的开发过程中使用命令行，最好不要使用IDE提供的UI。</p><p>当你了解了Git之后，也需要学习一下常见的git工作流：</p><ul><li>Git flow</li><li>Trunk-based</li><li>Feature branching</li><li>Forking Workflow</li></ul><p>学习资料:</p><ul><li>官方文档：<a href="https://git-scm.com/doc">Git - Documentation (git-scm.com)</a>. 官方文档已经写的很详尽了。</li><li>一本书 Pro Git: <a href="https://github.com/progit/progit2/releases/download/2.1.338/progit.pdf">https://github.com/progit/progit2/releases/download/2.1.338/progit.pdf</a></li></ul>]]>
    </content>
    <id>https://www.realks.com/2022/03/24/how-to-learning-git/</id>
    <link href="https://www.realks.com/2022/03/24/how-to-learning-git/"/>
    <published>2022-03-24T12:34:26.000Z</published>
    <summary>
      <![CDATA[<p>版本控制记录着软件的每一次改变，每一次发布，以及每一个Bug。 它贯穿于软件的生命周期，从生到死，<strong>请慎重对待每一次提交，像记录历史一样书写提交记录</strong>。</p>
<p>当下最流行的Git是一个不错的选择，<strong>作为合格的软件开发人员你应该熟练的使用它</strong>。你可以构造一些场景去练习git命令，比如：</p>]]>
    </summary>
    <title>新手如何学习Git</title>
    <updated>2022-03-24T12:34:26.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="Windows" scheme="https://www.realks.com/categories/Windows/"/>
    <category term="WSL" scheme="https://www.realks.com/categories/Windows/WSL/"/>
    <category term="wsl" scheme="https://www.realks.com/tags/wsl/"/>
    <category term="development" scheme="https://www.realks.com/tags/development/"/>
    <content>
      <![CDATA[<p>一篇平平无奇的WSL使用推荐指南。在本篇中，不会讨论什么是WSL，如何安装WSL，单纯地分享我是如何使用WSL进行日常开发的。</p><p>我当前是使用的WSL配置如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">NAME            STATE           VERSION</span><br><span class="line">* Ubuntu-20.04    Running         2</span><br></pre></td></tr></table></figure><p>我习惯使用大量的CLI来提升自己的工作效率以及使用体验，因此一个好用的Terminal和一系列高效率的CLI工具对我是十分重要的。</p><span id="more"></span><h2 id="WSL-配置"><a href="#WSL-配置" class="headerlink" title="WSL 配置"></a>WSL 配置</h2><p>文件的权限问题，解决方法参考<a href="https://chengqing.dev/2021/04/24/long-term-wsl/">[长期更新] WSL使用记录</a></p><p>配置git，执行下面命令之后，使用https clone的代码就不用经常输入用户名密码了。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git config --global credential.helper store</span><br></pre></td></tr></table></figure><p>WSL中Git的默认编辑器对于我来说不咋好用，我比较喜欢使用vim。可以使用下面的命令更改默认编辑器：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git config --global core.editor vim</span><br></pre></td></tr></table></figure><h2 id="CLI工具"><a href="#CLI工具" class="headerlink" title="CLI工具"></a>CLI工具</h2><h3 id="Terminal"><a href="#Terminal" class="headerlink" title="Terminal"></a>Terminal</h3><p>在Windows上面，我推荐使用 Windows Terminal （<a href="https://www.microsoft.com/store/productId/9N0DX20HK701">下载链接</a>）。推荐理由：</p><ul><li>微软官方开发维护。</li><li>可以根据自己的使用习惯定制化。</li><li>可以在WSL，PowerShell，CMD，Azure Cloud Shell之间切换。</li></ul><h3 id="Zsh"><a href="#Zsh" class="headerlink" title="Zsh"></a>Zsh</h3><p>ubuntu 里面默认提供的是Bash，不是特别适合日常使用。相较于Bash，Zsh更加适合交互，比如如下功能：</p><ul><li>无需 <code>cd</code> 也可以切换目录，在Bash需要使用 <code>cd projects</code> ，在Zsh下直接 <code>projects</code> 就行了。</li><li>扩展路径，比如输入 <code>/u/loc</code> 然后Tab，就会变成 <code>/usr/local/</code> 。</li><li>支持插件和主题。</li></ul><p>如何安装Zsh</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt-get install zsh -y</span><br></pre></td></tr></table></figure><p>如果想知道更多有关Zsh的知识，请阅读<a href="https://zsh.sourceforge.io/">官方文档</a>。</p><h3 id="Oh-My-Zsh"><a href="#Oh-My-Zsh" class="headerlink" title="Oh My Zsh"></a>Oh My Zsh</h3><p>Oh My Zsh 是一个开源的管理Zsh配置的框架。在其<a href="https://github.com/ohmyzsh/ohmyzsh">GitHub</a>的Readme中有一句很有意思的话，<strong>Oh My Zsh will not make you a 10x developer…but you may feel like one.</strong></p><p>如何安装Oh My Zsh：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt-get install wget -y</span><br><span class="line">sh -c <span class="string">&quot;<span class="subst">$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)</span>&quot;</span></span><br></pre></td></tr></table></figure><p>我使用的是Oh My Zsh的默认主题，如果想更改主题，请阅读 <a href="https://github.com/ohmyzsh/ohmyzsh#themes">Readme</a>.</p><h3 id="Oh-My-Zsh-插件"><a href="#Oh-My-Zsh-插件" class="headerlink" title="Oh My Zsh 插件"></a>Oh My Zsh 插件</h3><p>插件是一个重头戏，利用插件可以提升我们的工作效率。</p><p><code>autojump</code> 是一个小工具，可以帮助我们快速导航到一个目录中，支持模糊匹配。比如有一个目录名称我记得其中包含了 <code>hexo</code> 而且我曾经进入过，那么我就可以使用 <code>j hexo</code> 尝试进入。<strong>强烈推荐</strong>。</p><p>安装方式：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt-get install autojump -y</span><br></pre></td></tr></table></figure><p>安装完之后，修改 <code>~/.zshrc</code></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">plugins=(git autojump)</span><br></pre></td></tr></table></figure><p><code>git</code> 支持git的插件时oh-my-zsh 默认添加了的，提供了很多简写的别名。比如我常用的 <code>gst</code> 就是 <code>git status</code> 的别名。更多别名，请参考 <a href="https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/git">ohmyzsh&#x2F;plugins&#x2F;git</a>。</p><p>Oh My Zsh还支持很多插件，详情请参考<a href="https://github.com/ohmyzsh/ohmyzsh/wiki/Plugins">Plugins</a>。</p><h3 id="Homebrew"><a href="#Homebrew" class="headerlink" title="Homebrew"></a><strong>Homebrew</strong></h3><p>Homebrew 是一个包管理器。Homebrew也提供了很多有用的cli工具，我使用Homebrew 的原因是我公司配置的电脑是Macbook，在WSL中继续使用该工具，可以给我带来一致性的体验。而且Homebrew自己开发CLI工具，安装等相比于apt更加方便。</p><p>安装方式：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/bin/bash -c <span class="string">&quot;<span class="subst">$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)</span>&quot;</span></span><br></pre></td></tr></table></figure><p>更多使用方式，请参考<a href="https://brew.sh/">The Missing Package Manager for macOS (or Linux) — Homebrew</a></p><h3 id="tig"><a href="#tig" class="headerlink" title="tig"></a>tig</h3><p>在使用git的过程中，本地看提交记录，或者reset的时候找起点等等情况下，如果使用 <code>git log</code> 就会很难受，tig完美地解决了这个问题，还能快速浏览每一个提交的内容等。</p><p>安装方式：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt-get install tig -y</span><br><span class="line"></span><br><span class="line">or</span><br><span class="line"></span><br><span class="line">brew install tig</span><br></pre></td></tr></table></figure><h2 id="语言开发环境管理"><a href="#语言开发环境管理" class="headerlink" title="语言开发环境管理"></a>语言开发环境管理</h2><p>这里只是列举了一些，我使用过的管理工具，其它语言请自行探索。</p><table><thead><tr><th>语言</th><th>管理工具</th><th>安装方式</th><th>文档</th></tr></thead><tbody><tr><td>Java</td><td>jenv</td><td><code>brew install jenv</code></td><td><a href="https://github.com/jenv/jenv">jenv&#x2F;jenv: Manage your Java environment</a></td></tr><tr><td>Node.js</td><td>nvm</td><td><code>wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash</code></td><td><a href="https://github.com/nvm-sh/nvm">nvm-sh&#x2F;nvm: Node Version Manager - POSIX-compliant bash script to manage multiple active node.js versions</a></td></tr><tr><td>Ruby</td><td>rbenv</td><td><code>brew install rbenv</code></td><td><a href="https://github.com/rbenv/rbenv">rbenv&#x2F;rbenv: Manage your app’s Ruby environment</a></td></tr><tr><td>Python</td><td>pyenv</td><td><code>brew install pyenv</code></td><td><a href="https://github.com/pyenv/pyenv">Simple Python version management (from pyenv)</a></td></tr></tbody></table><h3 id="与JetBrains-IDE集成"><a href="#与JetBrains-IDE集成" class="headerlink" title="与JetBrains IDE集成"></a>与JetBrains IDE集成</h3><p>与JetBrains IDE集成，需要做两件事情。</p><ol><li>切换Terminal，默认的是CMD。为了更好的体验，可以切换到wsl.</li></ol><p><code>Settings</code> → <code>Tools</code>  → <code>Terminal</code> , 在 <code>Application Settings</code> 中 <code>Shell path</code> 改成 <code>wsl</code> .</p><img src="wsl-terminal.png" width="80%"><ol start="2"><li>设置SDK，以 WebStorm为例。</li></ol><img src="wsl-sdk.png" width="80%"><h3 id="VS-Code"><a href="#VS-Code" class="headerlink" title="VS Code"></a>VS Code</h3><ol><li>在vscode中安装扩展插件 <code>Remote Development</code> </li><li><code>Ctrl + Shift + P</code> , 输入 <code>shell command</code> , 执行 <code>Install &#39;code&#39; command in PATH command</code></li><li>在WSL中就可以使用vscode了，比如 <code>code .</code> 在vscode中打开当前目录。</li></ol>]]>
    </content>
    <id>https://www.realks.com/2022/01/13/wsl-development/</id>
    <link href="https://www.realks.com/2022/01/13/wsl-development/"/>
    <published>2022-01-13T16:00:44.000Z</published>
    <summary>
      <![CDATA[<p>一篇平平无奇的WSL使用推荐指南。在本篇中，不会讨论什么是WSL，如何安装WSL，单纯地分享我是如何使用WSL进行日常开发的。</p>
<p>我当前是使用的WSL配置如下：</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">NAME            STATE           VERSION</span><br><span class="line">* Ubuntu-20.04    Running         2</span><br></pre></td></tr></table></figure>

<p>我习惯使用大量的CLI来提升自己的工作效率以及使用体验，因此一个好用的Terminal和一系列高效率的CLI工具对我是十分重要的。</p>]]>
    </summary>
    <title>WSL开发环境搭建分享</title>
    <updated>2022-07-13T14:25:44.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="Homelab" scheme="https://www.realks.com/categories/Homelab/"/>
    <category term="Homelab" scheme="https://www.realks.com/tags/Homelab/"/>
    <category term="AWTRIX" scheme="https://www.realks.com/tags/AWTRIX/"/>
    <content>
      <![CDATA[<h2 id="为啥想写这个App"><a href="#为啥想写这个App" class="headerlink" title="为啥想写这个App"></a>为啥想写这个App</h2><p>21年的时候做桌面改造的时候，想给自己加一个时钟。看了一圈之后，最终入手了AWTRIX Pro mini。</p><p>在AWTRIX App Store中有很多有趣的App，比如GithubFollowers, Bilibili等，并且安装了GithubFollowers，一段时间之后，发现那个数字一直卡在7，尴尴尬尬，内心毫无波澜，于是卸掉。</p><p>于是就想看看自己自己博客有多少有效阅读量（每篇博客的阅读量之和）。</p><span id="more"></span><h2 id="如何实现"><a href="#如何实现" class="headerlink" title="如何实现"></a>如何实现</h2><p>这是我第一次开发AWTRIX App，我也是极其懵逼。阅读官方文档是最快捷的方法，如果有兴趣可以参考<a href="https://awtrixdocs.blueforcer.de/#/en-en/appcoding">Programming (blueforcer.de)</a></p><p>我的博客是使用Hexo搭建的，阅读计数使用的是LeanCloud。</p><p>LeanCloud是提供了API接口，文档参见 <a href="https://leancloud.cn/docs/rest_api.html">存储 REST API 使用指南 - LeanCloud 文档</a></p><h3 id="第一步：获取数据"><a href="#第一步：获取数据" class="headerlink" title="第一步：获取数据"></a>第一步：获取数据</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">Sub <span class="title function_">App_startDownload</span><span class="params">(jobNr As Int)</span></span><br><span class="line">Select jobNr</span><br><span class="line">Case <span class="number">1</span></span><br><span class="line">App.Download(App.get(<span class="string">&quot;API&quot;</span>)&amp;<span class="string">&quot;/1.1/classes/Counter&quot;</span>)</span><br><span class="line">App.Header = CreateMap(<span class="string">&quot;X-LC-Id&quot;</span>:App.get(<span class="string">&quot;AppId&quot;</span>), <span class="string">&quot;X-LC-Key&quot;</span>:App.get(<span class="string">&quot;AppKey&quot;</span>))</span><br><span class="line">End Select</span><br><span class="line">End Sub</span><br></pre></td></tr></table></figure><p>这一步需要注意的是 <code>App.get()</code> 中key的大小写，我在这里栽倒了。</p><h3 id="第二步：处理数据"><a href="#第二步：处理数据" class="headerlink" title="第二步：处理数据"></a>第二步：处理数据</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">Sub <span class="title function_">App_evalJobResponse</span><span class="params">(Resp As JobResponse)</span></span><br><span class="line">Try</span><br><span class="line">If Resp.success Then</span><br><span class="line">Select Resp.jobNr</span><br><span class="line">Case <span class="number">1</span></span><br><span class="line">Dim parser As JSONParser</span><br><span class="line">parser.Initialize(Resp.ResponseString)</span><br><span class="line">Dim root <span class="type">As</span> <span class="variable">Map</span> <span class="operator">=</span> parser.NextObject</span><br><span class="line">Dim results <span class="type">As</span> <span class="variable">List</span> <span class="operator">=</span> root.Get(<span class="string">&quot;results&quot;</span>)</span><br><span class="line">total_view = <span class="number">0</span></span><br><span class="line">For Each postView As Map In <span class="type">results</span></span><br><span class="line"><span class="variable">total_view</span> <span class="operator">=</span> total_view + postView.Get(<span class="string">&quot;time&quot;</span>)</span><br><span class="line">Next</span><br><span class="line">End Select</span><br><span class="line">End If</span><br><span class="line">Catch</span><br><span class="line">App.throwError(LastException)</span><br><span class="line">End Try</span><br><span class="line">End Sub</span><br></pre></td></tr></table></figure><p><code>total_view</code> 是一个全局变量，记得清零。</p><h3 id="第三步：显示输出"><a href="#第三步：显示输出" class="headerlink" title="第三步：显示输出"></a>第三步：显示输出</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Sub App_genFrame</span><br><span class="line">App.genSimpleFrame(total_view,<span class="number">1720</span>,True,False,Null,True)</span><br><span class="line">End Sub</span><br></pre></td></tr></table></figure><p>代码详见：<a href="https://github.com/chengqing-su/awtrix-hexo-leancloud-counter">https://github.com/chengqing-su/awtrix-hexo-leancloud-counter</a></p><p>如果你需要编译号的Jar包，请自取：<a href="https://github.com/chengqing-su/awtrix-hexo-leancloud-counter/releases/download/v1.0.0/HexoLeanCloud.tar.gz">https://github.com/chengqing-su/awtrix-hexo-leancloud-counter/releases/download/v1.0.0/HexoLeanCloud.tar.gz</a></p><h2 id="体验"><a href="#体验" class="headerlink" title="体验"></a>体验</h2><p>最近几天看着数字不对地变化，感觉自己更加有动力去写博客，去维护博客。</p><img src="show.jpg" width="80%">]]>
    </content>
    <id>https://www.realks.com/2022/01/12/awtrix-hexo-leancloud-counter/</id>
    <link href="https://www.realks.com/2022/01/12/awtrix-hexo-leancloud-counter/"/>
    <published>2022-01-12T13:57:25.000Z</published>
    <summary>
      <![CDATA[<h2 id="为啥想写这个App"><a href="#为啥想写这个App" class="headerlink" title="为啥想写这个App"></a>为啥想写这个App</h2><p>21年的时候做桌面改造的时候，想给自己加一个时钟。看了一圈之后，最终入手了AWTRIX Pro mini。</p>
<p>在AWTRIX App Store中有很多有趣的App，比如GithubFollowers, Bilibili等，并且安装了GithubFollowers，一段时间之后，发现那个数字一直卡在7，尴尴尬尬，内心毫无波澜，于是卸掉。</p>
<p>于是就想看看自己自己博客有多少有效阅读量（每篇博客的阅读量之和）。</p>]]>
    </summary>
    <title>AWTRIX 显示Hexo博客阅读数量</title>
    <updated>2022-01-12T13:57:25.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="DevOps" scheme="https://www.realks.com/categories/DevOps/"/>
    <category term="Version Control" scheme="https://www.realks.com/categories/DevOps/Version-Control/"/>
    <category term="DevOps" scheme="https://www.realks.com/tags/DevOps/"/>
    <category term="Version Control" scheme="https://www.realks.com/tags/Version-Control/"/>
    <content>
      <![CDATA[<p>本文将聊一聊版本控制和版本控制系统。</p><span id="more"></span><h2 id="版本控制"><a href="#版本控制" class="headerlink" title="版本控制"></a>版本控制</h2><p>在大学的期间，我们会以团队的方式完成一项大作业，比如写一个小编译器，基本上大作业都会要求写一份报告。</p><p>团队成员：A、B、C、D</p><p>报告结构：介绍、原理分析、设计、实现、总结和展望</p><p>分工：</p><p>A：完成介绍、总结和展望两部分，合并报告</p><p>B：完成原理分析部分</p><p>C：完成设计部分</p><p>D：完成实现部分</p><img src="version-control-1.drawio.svg" width="50%"><p>为了统一风格，比如标题字体、正文字体，以及大家各自完成各自的部分不用等待别人，因此我们会先做一个模版。之后A、B、C、D都会基于改模版去填充各自的内容。这时候我们就有了一个Word文件，我们姑且取名为 <code>homework-v0.docx</code>.</p><p>当B在开始写<em>原理分析</em>的时候，他需要先从A那边拿到， 然后复制一份<code>homework-v0.docx</code>并命名为<code>homework-v0.1.docx</code>。在新文件中开始自己的工作，写完之后保存。检查一遍之后，他发现有些小问题要改一下，因此复制<code>homework-v0.1.docx</code>并命名为<code>homework-v0.2.docx</code>。</p><p> 当B完成了自己的部分，这个时候需要将B完成的部分合并到模版中。复制<code>homework-v0.docx</code> 为<code>homework-v1.docx</code> ，在新文件中插入B完成的内容然后保存，这个时候就形成了v1版本的报告。</p><p>这个时候，我们有了两份文件：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">homework-v0.docx</span><br><span class="line">homework-v1.docx </span><br></pre></td></tr></table></figure><p>v0和v1之间的区别是什么？v1在v0的基础上新增了内容，而上文中v0.1和v0.2是更改了一些内容，我们称这些内容为变更。上文中，我们通过复制文件方式实现了版本的管理和追踪，实现了对变更的管理和追踪。</p><p>这只是版本控制的一种实现方式，一种最简单的实现方式。接下来再聊聊版本控制系统。</p><h2 id="版本控制系统"><a href="#版本控制系统" class="headerlink" title="版本控制系统"></a>版本控制系统</h2><p><strong>什么是版本控制系统，用于管理和追踪变更的工具</strong>。这里面的变更不仅仅是针对代码的，也可以是文档或者其他的工程文件等。</p><h3 id="本地版本控制系统-Local-Version-Control-Systems"><a href="#本地版本控制系统-Local-Version-Control-Systems" class="headerlink" title="本地版本控制系统(Local Version Control Systems)"></a>本地版本控制系统(<strong>Local Version Control Systems)</strong></h3><p>使用复制文件这种方式，很容易犯错，一不小心就会写错文件或者覆盖到意料之外的文件。</p><img src="lvcs.png" width="60%"><p>因此就有了<strong>本地版本控制系统，通常是采用数据库来记录文件的历次更新差异</strong>。本地版本控制系统是第一代版本控制系统，其代表是Revision Control System(RCS)。<a href="https://www.gnu.org/software/rcs/">RCS</a>的工作原理是将补丁（文件之间的差异）以一种特殊的方式保存在磁盘上，然后添加补丁的方式重新创建任何时间点的文件。</p><h3 id="集中版本控制系统-Centralized-Version-Control-Systems"><a href="#集中版本控制系统-Centralized-Version-Control-Systems" class="headerlink" title="集中版本控制系统(Centralized Version Control Systems)"></a>集中版本控制系统(<strong>Centralized Version Control Systems)</strong></h3><p>在本地版本控制系统的使用过程，不可避免的问题就是如何与其他开发者协同工作。</p><img src="cvcs.png" width="60%"><p>因此就有了集中版本控制系统，也就是第二代版本控制系统。相对于本地VCS，其提供了如下优点：</p><ul><li>项目透明度。项目成员可以知道项目中其他成员在做什么。</li><li>更加精确的权限控制。CVCS的管理员可以设置谁可以做什么。</li><li>更方便管理。</li></ul><p>当然也是有缺点的，最大的缺点就是单点故障。日常的开发协作是严重依赖于中心服务器，如果服务器挂了，项目成员将不能进行协作，如果服务器的磁盘损坏且无备份，有可能会损失整个历史记录。</p><p>其代表工具有CVS，Subversion(SVN)</p><h3 id="分布式版本控制系统-Distributed-Version-Control-Systems"><a href="#分布式版本控制系统-Distributed-Version-Control-Systems" class="headerlink" title="分布式版本控制系统(Distributed Version Control Systems)"></a>分布式版本控制系统(<strong>Distributed Version Control Systems)</strong></h3><p>分布式版本控制系统中，每一个客户端不是提取了最新的快照，而是镜像了整个代码仓库，包括历史记录等，如果某个服务器挂，可以使用任何客户端的存储库复制回服务器中以将其还原。</p><img src="dvcs.png" width="60%"><p>分布式版本控制系统，又称为第三代版本控制系统，其代表有BitKeeper、Git、Monotone、darcs、Mercurial。目前Git是主流的选择。</p><p>以上部分，关于<em>本地版本控制系统，集中版本控制系统，分布式版本控制系统</em> 主要来自于<a href="https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control">Git的官网</a>，只有部分内容，详细内容请阅读官方资料。</p><h2 id="为什么需要版本控制系统"><a href="#为什么需要版本控制系统" class="headerlink" title="为什么需要版本控制系统"></a>为什么需要版本控制系统</h2><ol><li>协作。在一个多人的项目中，项目成员更加方便地贡献代码，更加快速地高效地采用其他成员贡献的代码。</li><li>版本存储。</li><li>回滚。因为有版本存在，可以快速回滚到什么一个版本。</li><li>追踪变更历史。可以清楚地知道整个代码库中，谁在什么时间做出了什么改动，改动的具体内容是什么。</li><li>备份。备份时分布式版本控制系统附带一个非常好用的功能，每个项目成员都拥有完整的代码副本，当服务器挂了，也可以快速恢复。</li></ol><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control">About Version Control (git-scm.com)</a></p><p><a href="https://www.atlassian.com/git/tutorials/what-is-version-control">what is version-control</a></p>]]>
    </content>
    <id>https://www.realks.com/2022/01/05/version-control/</id>
    <link href="https://www.realks.com/2022/01/05/version-control/"/>
    <published>2022-01-05T15:32:30.000Z</published>
    <summary>
      <![CDATA[<p>本文将聊一聊版本控制和版本控制系统。</p>]]>
    </summary>
    <title>版本控制</title>
    <updated>2022-01-05T15:32:30.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="Life" scheme="https://www.realks.com/categories/Life/"/>
    <category term="2020" scheme="https://www.realks.com/tags/2020/"/>
    <category term="总结" scheme="https://www.realks.com/tags/%E6%80%BB%E7%BB%93/"/>
    <category term="2021" scheme="https://www.realks.com/tags/2021/"/>
    <content>
      <![CDATA[<p>2021年，于我而言是变化极多的一年。总体趋势是好的，我看见了更多的可能和更多的希望。</p><p>还完了贷款。</p><p>换了一份工作。离开了ThoughtWorks，加入了SAP。</p><p>完成了第一场纯英语Session（2021年10月28日）。</p><p>学会了游泳。</p><p>做了一次桌面改造。</p><p>接种了新冠疫苗。这是我小学几年级之后第一次接种疫苗，小学接种完疫苗之后没多久就得了脑膜炎，虽然二者没有啥相关性，但是还是怕。</p><p>入手了一个扫地机器人。</p><p>组装了一台电脑。6月组装电脑，年底发12代，有一种49年入国军的感觉。</p><p>新入手了一台NAS。目前总存储应该超过了30T了。</p><p>……</p><span id="more"></span><h2 id="2021年"><a href="#2021年" class="headerlink" title="2021年"></a>2021年</h2><h3 id="完成了的目标"><a href="#完成了的目标" class="headerlink" title="完成了的目标"></a>完成了的目标</h3><ul><li>考证计划。 Azure Developer Associate</li><li>在新加坡看一场电影。在新加坡看了一场电影，忘记名字了（看来是真的老了）。</li><li>小说。追完《临渊行》和《帅教官》（一如既往的草草结尾），目前没啥网络小说可以追了。</li><li>小破站。对小破站的上的内容开始有点挑剔了，知识性内容的差距和国外某站还是很大的。今年发现一个比较给力的UP主 先看测评。</li><li>回国。提桶跑路，比预想的回国早了很多。</li></ul><h3 id="没有完成的目标"><a href="#没有完成的目标" class="headerlink" title="没有完成的目标"></a>没有完成的目标</h3><ul><li>52篇博客。数了一下，只完成了17篇。</li><li>把《中国乡村》看完。社科的书籍是真难啃。今年看了《实践论》《矛盾论》，伟人就是伟人。</li></ul><h3 id="吐槽"><a href="#吐槽" class="headerlink" title="吐槽"></a>吐槽</h3><p><strong>刚跑路，前东家就上市了。</strong></p><p>今年不适合理财，或者说我不适合理财。</p><p>在坡县觉得没地方可去，回国后是啥地方也没有去（就回了一趟家，然后去了一趟深圳）。</p><p>至今买不起显卡，现在的显卡连帝国时代都带不动。</p><h2 id="2022年"><a href="#2022年" class="headerlink" title="2022年"></a>2022年</h2><p>2022年，要有规律。早睡早起好身体。<br>2022年，要有朝气。<br>2022年，要多出去走走。</p><h3 id="新的目标"><a href="#新的目标" class="headerlink" title="新的目标"></a>新的目标</h3><ol><li>减肥20斤，76kg→66kg，1-5月每个月瘦4斤，6-8月保持，9-12月争取每月瘦1斤。</li><li>加强Python,Java,Ruby,Typescript,PHP, 学习Go。</li><li>每季度阅读一本社科人文书籍。</li><li>每月输出3-4篇有效有质量的博客。</li><li>每月读阅读一本技术书籍。</li><li>证书目标：Azure DevOps(1-2月)，Azure Solutions Architect Expert(5-6月), AWS Certified Solutions Architect – Professional（3-4月）</li><li>每天坚持写日志。</li><li>每周至少做一次饭（正餐）。</li></ol>]]>
    </content>
    <id>https://www.realks.com/2021/12/30/2021-2022/</id>
    <link href="https://www.realks.com/2021/12/30/2021-2022/"/>
    <published>2021-12-30T12:22:47.000Z</published>
    <summary>
      <![CDATA[<p>2021年，于我而言是变化极多的一年。总体趋势是好的，我看见了更多的可能和更多的希望。</p>
<p>还完了贷款。</p>
<p>换了一份工作。离开了ThoughtWorks，加入了SAP。</p>
<p>完成了第一场纯英语Session（2021年10月28日）。</p>
<p>学会了游泳。</p>
<p>做了一次桌面改造。</p>
<p>接种了新冠疫苗。这是我小学几年级之后第一次接种疫苗，小学接种完疫苗之后没多久就得了脑膜炎，虽然二者没有啥相关性，但是还是怕。</p>
<p>入手了一个扫地机器人。</p>
<p>组装了一台电脑。6月组装电脑，年底发12代，有一种49年入国军的感觉。</p>
<p>新入手了一台NAS。目前总存储应该超过了30T了。</p>
<p>……</p>]]>
    </summary>
    <title>2021干得不错,2022继续加油</title>
    <updated>2021-12-30T12:22:47.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="DevOps" scheme="https://www.realks.com/categories/DevOps/"/>
    <category term="Gitlab" scheme="https://www.realks.com/categories/DevOps/Gitlab/"/>
    <category term="DevOps" scheme="https://www.realks.com/tags/DevOps/"/>
    <category term="Gitlab" scheme="https://www.realks.com/tags/Gitlab/"/>
    <category term="手动Job" scheme="https://www.realks.com/tags/%E6%89%8B%E5%8A%A8Job/"/>
    <content>
      <![CDATA[<p>在持续交付的过程中，需要手动确认，然后才能继续部署到生产环境。在本文中将实现这个过程，也是一个踩坑的过程。</p><span id="more"></span><h2 id="如何实现"><a href="#如何实现" class="headerlink" title="如何实现"></a>如何实现</h2><p>说出来其实很简单，如下所示：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">waiting-for-approval:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">waiting-for-approval</span></span><br><span class="line">  <span class="attr">when:</span> <span class="string">manual</span></span><br><span class="line">  <span class="attr">allow_failure:</span> <span class="literal">false</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;waiting-for-approval&quot;</span></span><br></pre></td></tr></table></figure><p>这里面有两个关键字段： <code>when</code> 和 <code>allow_failure</code> 。</p><h3 id="when"><a href="#when" class="headerlink" title="when"></a><code>when</code></h3><p>该字段指示在什么条件下执行该Job。可以使用如下值,</p><ul><li><code>on_success</code> (默认): 当上一个stage中的所有Job成功执行之后才能执行或者上一个stage中所有的Job 配置有字段<code>allow_failure: true</code> 。</li><li><code>manual</code>: 手动触发该Job。</li><li><code>always</code>: 无论之前的stage是否成功，总是执行该Job。</li><li><code>on_failure</code>: 当上一个stage中有Job失败的情况下，执行该Job。</li><li><code>delayed</code>: 延迟一段时间执行该Job。</li><li><code>never</code>: 不执行该Job.</li></ul><h3 id="allow-failure"><a href="#allow-failure" class="headerlink" title="allow_failure"></a><code>allow_failure</code></h3><p>该字段决定了当前Job执行失败的情况下，是否继续执行pipeline。值可以是<code>true</code> 或者<code>false</code> 。</p><p><strong>注意其默认值，这是比较坑的一点</strong></p><ul><li>当Job有 <code>when: true</code> 时，默认值为 <code>true</code></li><li>当Job有 <code>when: true</code> 并且配置了 <code>rules</code> ，默认值为 false</li><li>其他情况，默认值为 <code>true</code></li></ul><h2 id="踩坑过程"><a href="#踩坑过程" class="headerlink" title="踩坑过程"></a>踩坑过程</h2><h3 id="踩坑"><a href="#踩坑" class="headerlink" title="踩坑"></a>踩坑</h3><p>我想实现如所示pipeline</p><img src="ci-demo.png" width="80%"><p><code>.gitlab-ci.yml</code> 如下：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">image:</span> <span class="string">alpine</span></span><br><span class="line"><span class="attr">stages:</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">test</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">build</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">deploy-to-qa</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">waiting-for-approval</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">deploy-to-prod</span></span><br><span class="line"></span><br><span class="line"><span class="attr">test:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">test</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;test&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">build:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">build</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;build&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">deploy-to-qa:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">deploy-to-qa</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;deploy-to-qa&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">waiting-for-approval:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">waiting-for-approval</span></span><br><span class="line">  <span class="attr">when:</span> <span class="string">manual</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;waiting-for-approval&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">deploy-to-prod:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">deploy-to-prod</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;deploy-to-prod&quot;</span></span><br></pre></td></tr></table></figure><p>得到执行结果如下：<br><img src="pipeline-stream.png" width="80%"></p><p>我还没有点approval，怎么就执行 <code>deploy-to-prod</code> 了？又看了一眼pipeline的状态是 <code>passed</code> ，怎么就 <code>passed</code> 了？<br><img src="pipeline-status.png" width="80%"></p><h3 id="坑在哪里？"><a href="#坑在哪里？" class="headerlink" title="坑在哪里？"></a>坑在哪里？</h3><p>又去看看了文档<a href="https://docs.gitlab.com/ee/ci/yaml/#when">Keyword reference for the <code>.gitlab-ci.yml</code> file | GitLab</a> ，在 <strong><code>Additional details</code></strong> 中发现 了下面这段话</p><blockquote><p>The default behavior of <code>allow_failure</code> changes to <code>true</code> with <code>when: manual</code>. However, if you use <code>when: manual</code> with <code>[rules](https://docs.gitlab.com/ee/ci/yaml/#rules)</code>, <code>allow_failure</code> defaults to <code>false</code>.</p></blockquote><p>谜题解开了！</p><p>之后又找到另外一篇文档 <a href="https://docs.gitlab.com/ee/ci/jobs/job_control.html#create-a-job-that-must-be-run-manually">Choose when to run jobs | GitLab</a> ，创建一个必须被手动触发的Job。</p><h3 id="填坑"><a href="#填坑" class="headerlink" title="填坑"></a>填坑</h3><p>修改<code>.gitlab-ci.yml</code> ，如下所示：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">image:</span> <span class="string">alpine</span></span><br><span class="line"><span class="attr">stages:</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">test</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">build</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">deploy-to-qa</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">waiting-for-approval</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">deploy-to-prod</span></span><br><span class="line"></span><br><span class="line"><span class="attr">test:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">test</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;test&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">build:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">build</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;build&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">deploy-to-qa:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">deploy-to-qa</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;deploy-to-qa&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">waiting-for-approval:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">waiting-for-approval</span></span><br><span class="line">  <span class="attr">when:</span> <span class="string">manual</span></span><br><span class="line">  <span class="attr">allow_failure:</span> <span class="literal">false</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;waiting-for-approval&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">deploy-to-prod:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">deploy-to-prod</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">echo</span> <span class="string">&quot;deploy-to-prod&quot;</span></span><br></pre></td></tr></table></figure><p>其执行结果如下：<br><img src="pipeline-stream-2.png" width="80%"><br><img src="pipeline-status-2.png" width="80%"></p>]]>
    </content>
    <id>https://www.realks.com/2021/12/20/gitlab-waiting-for-approval/</id>
    <link href="https://www.realks.com/2021/12/20/gitlab-waiting-for-approval/"/>
    <published>2021-12-20T11:38:52.000Z</published>
    <summary>
      <![CDATA[<p>在持续交付的过程中，需要手动确认，然后才能继续部署到生产环境。在本文中将实现这个过程，也是一个踩坑的过程。</p>]]>
    </summary>
    <title>Gitlab pipeline 等待手动操作</title>
    <updated>2021-12-20T11:38:52.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="DevOps" scheme="https://www.realks.com/categories/DevOps/"/>
    <category term="Gitlab" scheme="https://www.realks.com/categories/DevOps/Gitlab/"/>
    <category term="DevOps" scheme="https://www.realks.com/tags/DevOps/"/>
    <category term="Gitlab" scheme="https://www.realks.com/tags/Gitlab/"/>
    <category term="动态pipeline" scheme="https://www.realks.com/tags/%E5%8A%A8%E6%80%81pipeline/"/>
    <content>
      <![CDATA[<p>在pipeline的最佳实践中，不推荐使用动态pipeline。任何代码的可读性都是至关重要的，一旦开始使用动态pipeline就很难保证可读性，甚至无法保证可维护性。</p><p>虽然不推荐使用动态pipeline，但是在某些场景之下，使用动态pipeline会帮助我们在保证可读性不变甚至提高的情况下，同时提高了可维护性，这个时候我们推荐使用动态pipeline。</p><span id="more"></span><p>比如，我们需要在CI上对同一个项目跑100个测试，这一个测试唯一的区别就是传入参数不一样，这些参数会随着我们产品的演进而进行更新，比如如下一个片段重复100次，是不是会很痛苦？</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">test-with-arg-100:</span></span><br><span class="line">  <span class="attr">image:</span> <span class="string">ubuntu</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">test</span></span><br><span class="line">  <span class="attr">script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">auto/test</span> <span class="number">100</span></span><br></pre></td></tr></table></figure><h2 id="父子pipeline（Parent-child-pipeline）"><a href="#父子pipeline（Parent-child-pipeline）" class="headerlink" title="父子pipeline（Parent-child pipeline）"></a>父子pipeline（<strong>Parent-child pipeline）</strong></h2><p>在Gitlab CI&#x2F;CD 中，父子pipeline就是在一个pipeline中嵌套执行另外一个pipeline配置文件，即子pipeline。</p><p>子管道类型：</p><ul><li>合并请求子pipeline（Merge request child pipelines）</li><li>动态子pipeline（Dynamic child pipelines）</li><li>嵌套子pipeline（Nested child pipelines）</li></ul><p>更多内容可以参考<a href="https://docs.gitlab.com/ee/ci/pipelines/parent_child_pipelines.html">官方文档</a></p><h2 id="案例：使用动态子pipeline部署多个应用"><a href="#案例：使用动态子pipeline部署多个应用" class="headerlink" title="案例：使用动态子pipeline部署多个应用"></a>案例：使用动态子pipeline部署多个应用</h2><p>在我的Homelab环境中，有一个Infrastructure的Kubernetes集群，集群中需要部署一系列的基础应用，比如：</p><ul><li>external dns用于注册ingress dns到DNS server上。</li><li>Hashicorp Vault、Jenkins等实验应用</li><li>Prometheus等监控应用</li></ul><p>每一个应用都有一个对应的部署脚本，如下所示。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">├── auto</span><br><span class="line">│   ├── deploy-elasticsearch</span><br><span class="line">│   ├── deploy-exdns-homelab-local</span><br><span class="line">│   ├── deploy-grafana</span><br><span class="line">│   ├── deploy-hashicorp-vault</span><br><span class="line">│   ├── deploy-influxdb</span><br><span class="line">│   ├── deploy-ingress</span><br><span class="line">│   ├── deploy-jenkins</span><br><span class="line">│   ├── deploy-prometheus</span><br><span class="line">│   ├── deploy-sonarqube</span><br><span class="line">│   └── deploy-vsphere-prometheus</span><br></pre></td></tr></table></figure><p>上面的部署脚本，我都是使用 <code>auto/deploy-*</code> 这样的模式命名部署脚本的。这样根据部署脚本规律生成子pipeline的YAML配置文件，生成脚本如下。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash -eu</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> <span class="string">&quot;<span class="subst">$(dirname <span class="string">&quot;<span class="variable">$0</span>&quot;</span>)</span>/..&quot;</span></span><br><span class="line"></span><br><span class="line">CI_CONFIG_FILE=<span class="string">&quot;child-ci.yml&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">cat</span> &lt;&lt;<span class="string">EOF &gt; &quot;$&#123;CI_CONFIG_FILE&#125;&quot;</span></span><br><span class="line"><span class="string">image: $CI_REGISTRY_IMAGE</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">default:</span></span><br><span class="line"><span class="string">  retry: 2</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">variables:</span></span><br><span class="line"><span class="string">  KUBERNETES_SERVICE_ACCOUNT_OVERWRITE: k8s-infra-admin</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">stages:</span></span><br><span class="line"><span class="string">  - deploy</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">EOF</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> script_name <span class="keyword">in</span> auto/deploy-* ; <span class="keyword">do</span></span><br><span class="line">      <span class="built_in">cat</span> &lt;&lt;<span class="string">EOF &gt;&gt; &quot;$&#123;CI_CONFIG_FILE&#125;&quot;</span></span><br><span class="line"><span class="string">$&#123;script_name//\//-&#125;:</span></span><br><span class="line"><span class="string">  stage: deploy</span></span><br><span class="line"><span class="string">  script:</span></span><br><span class="line"><span class="string">    - $&#123;script_name&#125;</span></span><br><span class="line"><span class="string">  only:</span></span><br><span class="line"><span class="string">    - main</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">EOF</span></span><br><span class="line"><span class="keyword">done</span></span><br></pre></td></tr></table></figure><p>部分pipeline配置文件 <code>.gitlab-ci.yml</code> 如下：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">stages:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">generate-ci</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">run-ci</span></span><br><span class="line"><span class="attr">default:</span></span><br><span class="line">  <span class="attr">retry:</span> <span class="number">2</span></span><br><span class="line"></span><br><span class="line"><span class="attr">generate-config:</span></span><br><span class="line">  <span class="attr">image:</span> <span class="string">alpine:3.12</span></span><br><span class="line">  <span class="attr">before_script:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">apk</span> <span class="string">--no-cache</span> <span class="string">add</span> <span class="string">bash</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">generate-ci</span></span><br><span class="line">  <span class="attr">script:</span> <span class="string">auto/generate-ci-config</span></span><br><span class="line">  <span class="attr">artifacts:</span></span><br><span class="line">    <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">child-ci.yml</span></span><br><span class="line"></span><br><span class="line"><span class="attr">child-pipeline:</span></span><br><span class="line">  <span class="attr">stage:</span> <span class="string">run-ci</span></span><br><span class="line">  <span class="attr">trigger:</span></span><br><span class="line">    <span class="attr">strategy:</span> <span class="string">depend</span></span><br><span class="line">    <span class="attr">include:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">artifact:</span> <span class="string">child-ci.yml</span></span><br><span class="line">        <span class="attr">job:</span> <span class="string">generate-config</span></span><br></pre></td></tr></table></figure><p>pipeline执行图如下所示：<br><img src="pipeline.png" width="80%"></p>]]>
    </content>
    <id>https://www.realks.com/2021/12/11/gitlab-dynamic-pipeline/</id>
    <link href="https://www.realks.com/2021/12/11/gitlab-dynamic-pipeline/"/>
    <published>2021-12-11T05:07:20.000Z</published>
    <summary>
      <![CDATA[<p>在pipeline的最佳实践中，不推荐使用动态pipeline。任何代码的可读性都是至关重要的，一旦开始使用动态pipeline就很难保证可读性，甚至无法保证可维护性。</p>
<p>虽然不推荐使用动态pipeline，但是在某些场景之下，使用动态pipeline会帮助我们在保证可读性不变甚至提高的情况下，同时提高了可维护性，这个时候我们推荐使用动态pipeline。</p>]]>
    </summary>
    <title>Gitlab CI/CD 之 动态pipeline</title>
    <updated>2021-12-11T05:07:20.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="Life" scheme="https://www.realks.com/categories/Life/"/>
    <category term="桌面改造" scheme="https://www.realks.com/tags/%E6%A1%8C%E9%9D%A2%E6%94%B9%E9%80%A0/"/>
    <content>
      <![CDATA[<p>2021年6月2日，解除隔离，被放出来。时隔半年，我又回到了我的小窝。</p><p>在当时，可以预见的是，下一份工作可以长期在家办公（WFH），因此桌面还是需要改造一下，让自己在未来的生活和工作满意，取悦自己。</p><span id="more"></span><h2 id="之前的桌面"><a href="#之前的桌面" class="headerlink" title="之前的桌面"></a>之前的桌面</h2><p>先看一下之前的桌面，</p><img src="before.jpg" width="80%"><p>主要部件如下：</p><ol><li>23.8寸显示器（<strong>AOC Q2490PXQ</strong>）x2</li><li>屏幕挂灯(<strong>倍思</strong>) x2</li><li>摄像头（<strong>罗技 C310</strong>）x1</li><li>台灯（<strong>小米 米家LED智能台灯1S</strong>）x1</li><li>HUB (<strong>ORICO 全铝高速USB3.1Gen2</strong>) x1</li><li>HDMI 切换器（<strong>威迅 HDMI切换器二进一出 4K</strong>) x1</li><li>KVM切换器（<strong>绿联 KVM切换器 HDMI切屏器2进1出4K高清</strong>）x1</li><li>USB充电器 (<strong>小米 原装60W USB充电器快充版 六口输出 QC3.0快充协议</strong>) x1</li><li>温湿度传感器（<strong>小米 米家蓝牙温湿度计2</strong>）x1</li><li>插排（<strong>公牛（BULL） 防过充插座带多口全USB插排</strong>）x1</li><li><strong>微软（Microsoft）Designer 无线蓝牙鼠标</strong> x1</li><li>键盘（<strong>TT G821 青轴</strong>）x1</li><li><strong>罗技（Logitech） M720 鼠标</strong> x1</li><li>鼠标垫（<strong>宜适酷 典雅黑 BAS1801-01</strong>）x1</li><li>桌子（宜家 <strong>利蒙 150x75桌面 + 可调节桌腿</strong>）x 1</li><li>电脑椅（<strong>联丰 ds-177黑</strong>）x1</li></ol><p>主要问题：</p><ol><li>显示器屏幕分辨率为2k。虽然2k，但是内心却是向往4k。2k的屏敲代码一点儿都不开心。</li><li>摄像头不支持Windows Hello</li><li>桌面太乱了</li></ol><h2 id="第一部分改造"><a href="#第一部分改造" class="headerlink" title="第一部分改造"></a>第一部分改造</h2><h3 id="显示器"><a href="#显示器" class="headerlink" title="显示器"></a>显示器</h3><p>选择显示器最重要的是解决第一个问题，让我可以开心地码字。</p><p>第一步，确定确定尺寸。我桌面宽度是75cm，之前的是显示器是23.8寸，感觉还是比较舒适的，因此<strong>尺寸范围是23.8-27</strong>。</p><p>第二步，确定分辨率。之前的显示器是2560<em>1440dpi，文字显示的细腻度虽然比1080p好很多，但是依然有点糊以及有锯齿。我使用显示器的主要场景是写代码，所以文字显示细腻度对我来说是很重要的，因此选择*<em>分辨率为4K</em></em>。</p><p>第三步，确定其他功能。我长期面对屏幕，因此<strong>护眼</strong>对我比较重要。</p><p>第四步，显示器品牌。我个人更加倾向于DELL，LG这一类的大厂，也包括之前使用过的AOC。</p><p><strong>最终的选择是DELL P2721Q。</strong></p><p>最开始的时候购入的并不是DELL P2721Q，而是<strong>AOC U27U2D</strong>。从参数上来说AOC U27U2D完全满足我的需求，但是使用了一天之后，有一丢丢眩晕，然后7天无理由退货，重新购入DELL P2721Q。选择显示器的时候，重要的还是自己的眼睛舒服。</p><h3 id="屏幕挂灯"><a href="#屏幕挂灯" class="headerlink" title="屏幕挂灯"></a>屏幕挂灯</h3><p>之前的屏幕挂灯使用的是<strong>倍思</strong>的，其开关和调节按钮均在挂灯，每次开关都需要站起来，很不方便。</p><p>更换成<strong>小米</strong>的显示器挂灯。这款挂灯最大的优势就是够用而且便宜。</p><h3 id="摄像头"><a href="#摄像头" class="headerlink" title="摄像头"></a>摄像头</h3><p>之前使用的罗技C310摄像头是2017年购入，但是主要使用还是在2020年。主要的使用场景就是Zoom视频会议，除此之外没啥太大功能。</p><p>更换摄像头最主要的理由还是想使用Windows Hello。我使用1Password管理自己几乎所有的密码，使用了Windows Hello之后，我就不用每次输入PIN码或者密码了。</p><p>Windows Hello摄像头可选项特别少，当然淘宝上也还是有不少自行改装的。下面是几款我知道的支持Windows hello 的摄像头</p><ul><li>Intel RealSense SR300 F200 （3D结构光）</li><li>联想500 （红外）</li><li>罗技（Logitech）C1000e （红外）</li></ul><p>我购入的是联想500（购入时价格399）。理由就是相较于罗技C1000e（京东报价1499）便宜，而且当时没发现Intel RealSense F200（淘宝200多）。</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>2021年6月，第一部分改造到这里就结束，主要是对一部分硬件进行了升级和更换。</p><img src="in-progress.jpg" width="80%"><h2 id="第二部分改造"><a href="#第二部分改造" class="headerlink" title="第二部分改造"></a>第二部分改造</h2><p>2021年6月21日，加入我现在的公司，开始了长期的在家办公。</p><p>第二部分改造起因如下：</p><ul><li>2021年10月的时候发现，宜家利蒙桌子的桌面弯曲了</li><li>电脑椅也歪了</li></ul><p>改造目标：</p><ul><li>电动升降桌</li><li>电竞椅&#x2F;人体工学椅</li><li>无线桌面。桌面整洁，尽可能的没有线之类的。</li></ul><h3 id="电动升降桌"><a href="#电动升降桌" class="headerlink" title="电动升降桌"></a>电动升降桌</h3><p>为什么选择电动升降桌？</p><p>久坐的危害是非常大的，比如人脑供血不足，全身肌肉酸痛，脖子僵硬，痔疮，肥胖等等。坐站交替可以有效的缓解这些问题，电动升降桌就提供了站着办公的可能性。</p><p>如何选购可参考如下链接：</p><p><a href="https://zhuanlan.zhihu.com/p/158779121">2021年电动升降桌选购攻略及高性价比电动升降桌推荐（20210628更） - 知乎 (zhihu.com)</a></p><p><a href="https://zhuanlan.zhihu.com/p/383385657">电动升降桌推荐|电动升降桌选购指南 - 知乎 (zhihu.com)</a></p><p>我最终购入的是**Brateck北弧 K33。*<em>桌面大小，我个人还是倾向于选择150</em>75的。使用了快一个月了，够用但是并不优秀，比如：</p><ul><li>站立办公打字的时候，会有轻微的晃动，在我的可接受范围内。</li><li>加上桌面最低高度76cm，有点略高，使用时需要将椅子调整到最高位置。</li></ul><p>综合其价格和配置，总体上还是满意的。</p><h3 id="电脑椅"><a href="#电脑椅" class="headerlink" title="电脑椅"></a>电脑椅</h3><p>我购入的电脑椅是黑白调的电竞椅，入手这款椅子的原因：</p><ol><li>皮质，不容易积灰。</li><li>配色，红黑配。我挺喜欢红黑配色。</li></ol><p>我比较喜欢在疲惫的时候小憩一会儿或者单纯闭着眼睛躺着思考，这款电脑椅支持后躺，但是并不完美。当坐着的时候靠背有一定的支持作用，比较合适。但是躺下的时候，颈部是悬空的，很难受，加一个颈枕可以缓解这个痛点。</p><p>电脑椅和电动升降桌，每一个拎出来看，都还行。但是1+1 &lt; 2啊，两个之间的高度不契合，因此我加了一个网易严选的乳胶坐垫。</p><h3 id="显示器增高架"><a href="#显示器增高架" class="headerlink" title="显示器增高架"></a>显示器增高架</h3><p>选择入手显示增高架的原因如下：</p><ol><li>站立办公和坐着办公的时候，显示器的高度是需要调节，让自己感到舒适。DELL显示器支持的调节范围并不在我的舒适范围内。</li><li>增加桌面可利用空间。</li></ol><p>最终是在淘宝上购入的一款120cm的实木增高架。</p><img src="zeng-gao.jpg" width="80%"><p>在增高架上可以摆放一些小物价，比如书签、蓝牙温湿度传感器、便利贴等等。增高架最右边放着两个控制屏幕挂灯的旋转按钮。旁边是两个叠在一起的收纳盒，用于收纳数据线。将小米USB充电器使用3M魔术贴粘在了增高架上，可以进一步利用空间。这样剩下的空间就可以收纳键盘和鼠标。比如在看书或者吃饭的时候就可以把，键鼠收纳起来，桌面空间就足够大了。</p><h3 id="桌底收纳槽"><a href="#桌底收纳槽" class="headerlink" title="桌底收纳槽"></a>桌底收纳槽</h3><p>收纳桌下的各种线，是桌底下变得整洁。之前总是不小心踢掉某根线啥的。</p><img src="shou-na.jpg" width="80%"><p>在长81cm的收纳槽可以放下很多东西，一个10孔插排，一个KVM切换器，一个4空插排。可以使用束线带将多余的线收纳起来，这样线就不会特别乱。</p><h3 id="洞洞板"><a href="#洞洞板" class="headerlink" title="洞洞板"></a>洞洞板</h3><p>洞洞板可以灵活地收纳一些物品，比如将剪刀、手工刀、卷尺等挂在上面使用起来更加方便。</p><img src="dong-dong-ban.jpg" width="80%"><p>我选择的这一款带有一个托盘，托盘下面的空间放下扫地机器人，托盘上放打印机，这样可以更加有效率地利用空间。</p><h3 id="KVM切换器"><a href="#KVM切换器" class="headerlink" title="KVM切换器"></a>KVM切换器</h3><p>简单介绍一下我的使用场景，我有两台电脑，一台是公司的macbook pro，一台是自己组装的台式机。每天我都需要在这两个设备之间切换使用，工作的时候使用公司的电脑，非工作时间就使用自己的电脑。</p><p>之前的解决方案是使用了两个切换器，一个HDMI切换器用于切换其中一个屏幕，另外一个便宜的KVM的切换器用于切换屏幕和键鼠等外接设备。主要问题是，每次切换需要手动按两次按钮。</p><p>购入新的KVM切换器之后，可以使用鼠标在两个设备之间快速切换。而且因为不需要手动在切换器上操作，可以将其收纳到桌下理线槽上，进一步节省桌面空间。</p><p>到这里，第二部分改造就完成了。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><img src="finished.png" width="80%"><p>主要部件：</p><ol><li>屏幕挂灯(<strong>小米显示器挂灯</strong>) x2  <a href="https://item.jd.com/100007773997.html">京东</a></li><li>摄像头（<strong>联想500</strong>）x1</li><li>显示器（<strong>戴尔(DELL) P2721Q</strong>）x2 <a href="https://item.jd.com/100008883679.html">京东</a></li><li>120cm显示器增高架 x1 <a href="https://item.taobao.com/item.htm?spm=a1z09.2.0.0.31c42e8dqDa5uR&id=647103981604&_u=qenv8fsfe9d">淘宝</a></li><li>温湿度传感器（<strong>小米 米家蓝牙温湿度计2</strong>）x1 <a href="https://item.jd.com/100010622784.html">京东</a></li><li>收纳盒（<strong>JEKO 透明桌面收纳盒</strong>） x2 <a href="https://item.jd.com/100012243060.html">京东</a></li><li>USB充电器 (<strong>小米 原装60W USB充电器快充版 六口输出 QC3.0快充协议</strong>) x1</li><li>台灯（<strong>小米 米家LED智能台灯1S</strong>）x1 <a href="https://item.jd.com/100005676004.html">京东</a></li><li>键盘（<strong>TT G821 青轴</strong>）x1 <a href="https://item.jd.com/100010955206.html">京东</a></li><li>鼠标（<strong>罗技（Logitech） M720 鼠标</strong>） x1 <a href="https://item.jd.com/3903182.html">京东</a></li><li>鼠标垫（<strong>宜适酷 典雅黑 BAS1801-01</strong>）x1 <a href="https://item.jd.com/6383781.html">京东</a></li><li>桌子（<strong>Brateck升降桌K33</strong>) x1 <a href="https://item.jd.com/100018027300.html#crumb-wrap">京东</a></li><li>电脑椅（<strong>黑白调HDJY002BMJ</strong>）x1 <a href="https://item.jd.com/71134747428.html">京东</a></li><li>桌底收纳槽 x1 <a href="https://item.taobao.com/item.htm?spm=a1z09.2.0.0.31c42e8dqDa5uR&id=652211892542&_u=qenv8fse9b2">淘宝</a></li><li>KVM切换器（<strong>eKL 412HK</strong>）x1 <a href="https://item.jd.com/100009668205.html">京东</a></li><li>洞洞板 x1 <a href="https://item.taobao.com/item.htm?spm=a1z09.2.0.0.31c42e8dqDa5uR&id=609165789095&_u=qenv8fsb97f">淘宝</a></li></ol><p>通过这次改造之后，我能够更好地利用桌面空间，在不同的任务之间更好地切换。比如在看书的时候，可以把键鼠收纳到增高架下。利用增高架可以有效地将线缆隐藏起来，使用收纳槽可以更好地收纳线缆。</p><p>我也曾尝试使用磁吸模块将线缆固定在桌子侧边，这样桌面虽然看起来很整洁但是桌下依旧乱，因此暂时放弃了这种方案。<strong>即时收纳，物归原处才能够保持桌面的整洁</strong>。</p><p>到此刻位置，其实还有很多事情没有做，比如：</p><ul><li>无线充电</li><li>氛围灯</li><li>桌面时钟</li><li>提醒工具</li></ul><p>这些会在桌面改造2.0中慢慢来做，在未来的部分我更加倾向于自己DIY。</p>]]>
    </content>
    <id>https://www.realks.com/2021/10/26/refactor-desk-1-0/</id>
    <link href="https://www.realks.com/2021/10/26/refactor-desk-1-0/"/>
    <published>2021-10-26T15:23:12.000Z</published>
    <summary>
      <![CDATA[<p>2021年6月2日，解除隔离，被放出来。时隔半年，我又回到了我的小窝。</p>
<p>在当时，可以预见的是，下一份工作可以长期在家办公（WFH），因此桌面还是需要改造一下，让自己在未来的生活和工作满意，取悦自己。</p>]]>
    </summary>
    <title>桌面改造1.0</title>
    <updated>2021-10-26T15:23:12.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="Homelab" scheme="https://www.realks.com/categories/Homelab/"/>
    <category term="Homelab" scheme="https://www.realks.com/tags/Homelab/"/>
    <category term="Synology" scheme="https://www.realks.com/tags/Synology/"/>
    <category term="Active Backup for Business" scheme="https://www.realks.com/tags/Active-Backup-for-Business/"/>
    <category term="Esxi" scheme="https://www.realks.com/tags/Esxi/"/>
    <category term="vSphere" scheme="https://www.realks.com/tags/vSphere/"/>
    <category term="群晖" scheme="https://www.realks.com/tags/%E7%BE%A4%E6%99%96/"/>
    <category term="备份" scheme="https://www.realks.com/tags/%E5%A4%87%E4%BB%BD/"/>
    <content>
      <![CDATA[<p>TL;DR（太长不读版本）</p><p>大致思路就是，在Homelab环境中，利用Synology套件Active Backup for Business进行Vmware vSphere虚拟机备份。</p><hr><p>数据的灾备是很重要的，就像是开车系安全带，骑小摩托戴头盔，都是为了安全。在这篇博客中，将带着大家一起看看在利用群晖NAS备份vsphere中的虚拟机。</p><span id="more"></span><h2 id="Active-Backup-for-Business"><a href="#Active-Backup-for-Business" class="headerlink" title="Active Backup for Business"></a>Active Backup for Business</h2><p><code>Active Backup for Business</code>是群晖NAS的一个套件。</p><p>主要功能：</p><ul><li>支持Windows 服务器&#x2F;PC、Linux服务器、SMB&#x2F;rsync文件服务器以及VMware vSphere&#x2F;Microsoft Hyper-V虚拟机备份</li><li>灵活的计划和保留策略可自定义备份策略</li><li>支持备份数据还原，包括完整设备还原、即时还原和精细文件还原</li></ul><p>详细信息参考 <a href="%5Bhttps://www.synology.cn/zh-cn/dsm/6.2/software_spec/abb%5D(https://www.synology.cn/zh-cn/dsm/6.2/software_spec/abb)">Active Backup for Business技术规范</a></p><h2 id="从头开始创建一个备份任务"><a href="#从头开始创建一个备份任务" class="headerlink" title="从头开始创建一个备份任务"></a>从头开始创建一个备份任务</h2><h3 id="第一步：-安装并打开Active-Backup-for-Business"><a href="#第一步：-安装并打开Active-Backup-for-Business" class="headerlink" title="第一步： 安装并打开Active Backup for Business"></a>第一步： 安装并打开Active Backup for Business</h3><p>在套件中心中安装Active Backup for Business，该套件是免费的，很实在。</p><p>安装完成之后，打开它。</p><img src="synology-backup-1.png" width="80%"><h3 id="第二步：添加一个Hypervisor"><a href="#第二步：添加一个Hypervisor" class="headerlink" title="第二步：添加一个Hypervisor"></a>第二步：添加一个Hypervisor</h3><p>Hypervisor，也称为虚拟机器监视器或 VMM，是创建和运行虚拟计算机 （VM） 的软件。</p><p>按照下图添加你的esxi或者vCenter：</p><img src="synology-backup-2.png" width="80%"><p>添加完成之后，就可以在界面上看到Esxi或者vCenter的机器了，如下图所示。</p><img src="synology-backup-3.png" width="80%"><h3 id="第三步：创建备份任务"><a href="#第三步：创建备份任务" class="headerlink" title="第三步：创建备份任务"></a>第三步：创建备份任务</h3><img src="synology-backup-4.png" width="80%"><p>在创建过程中，我们通常会指定一个备份名称（上图中是vSphere-Task-1）， 然后可以选择是备份一台虚拟机还是多台虚拟机（上图中选中了openvpn-access-server），然后点击<em>下一步</em>。</p><img src="synology-backup-5.png" width="80%"><p>在列表中，选中一个共享文件夹用来存放备份数据。安装套件的时候会默认创建一个叫ActiveBackupforBusiness的共享文件夹。然后点击<em>下一步</em>。</p><img src="synology-backup-6.png" width="80%"><p>继续点击<em>下一步</em>。</p><img src="synology-backup-7.png" width="80%"><p>在这一步中是配置<em>备份任务</em>。可以使用默认配置，或者根据自己需求配置。然后点击<em>下一步</em>。</p><img src="synology-backup-8.png" width="80%"><p>这是一个检查页面，没问题就直接点击<em>下一步</em>。</p><img src="synology-backup-9.png" width="80%"><p>这一步就是很重要的一步，默认情况下是手动备份。这里推荐使用定时备份，这就不需要人为干预了。这样的备份也就更有意义了。然后<em>下一步</em>。</p><img src="synology-backup-10.png" width="80%"><p>这一块儿是设置保留策略，推荐使用。可以根据自己的情况进行选择，然后<em>下一步</em>。</p><img src="synology-backup-11.png" width="80%"><p>这一块儿是设置恢复权限，然后<em>下一步</em>。</p><img src="synology-backup-13.png" width="80%"><p>整个备份任务的总结，检查一下，没有问题就点击<em>完成</em>。这时候会有弹窗弹出，讯问是否现在备份，可根据自己的实际情况进行选择。</p><img src="synology-backup-14.png" width="80%"><p>到任务列表中，我们就可以看到刚才创建的备份任务以及上一次备份状态和下一次备份时间。</p><h3 id="第四步：备份"><a href="#第四步：备份" class="headerlink" title="第四步：备份"></a>第四步：备份</h3><p>定时备份任务会定时被触发，而手动备份就需要自己手动触发。</p><h2 id="恢复"><a href="#恢复" class="headerlink" title="恢复"></a>恢复</h2><img src="synology-restore-1.png" width="80%"><p>在虚拟机列表中选中已经备份了的虚拟机，然后点击<em>恢复</em></p><img src="synology-restore-2.png" width="80%"><p>从版本列表中选择一个备份版本，然后<em>下一步</em>。</p><img src="synology-restore-3.png" width="80%"><p>选择恢复到VMware vSphere，然后<em>下一步</em></p><img src="synology-restore-4.png" width="80%"><p>选择快速恢复还是完全恢复。</p><p>快速恢复：将NAS的磁盘挂载到Esxi上作为Datastore。</p><p>完全恢复：将备份数据复制到Esxi的Datastore中。</p><p>根据自己的实际情况选择，然后<em>下一步</em>。</p><img src="synology-restore-5.png" width="80%"><p>选择虚拟机恢复到哪里，支持原路径恢复，也可以更改到新的位置。然后<em>下一步</em>。</p><img src="synology-restore-6.png" width="80%"><p>检查一下，没有问题就点击<em>完成</em>，开始恢复。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在Homelab环境中，利用NAS对数据备份可以有效地保证数据安全。</p><p>重要的数据一定要多备份。</p>]]>
    </content>
    <id>https://www.realks.com/2021/10/19/homelab-backup-vsphere-vm-to-synology/</id>
    <link href="https://www.realks.com/2021/10/19/homelab-backup-vsphere-vm-to-synology/"/>
    <published>2021-10-19T13:17:19.000Z</published>
    <summary>
      <![CDATA[<p>TL;DR（太长不读版本）</p>
<p>大致思路就是，在Homelab环境中，利用Synology套件Active Backup for Business进行Vmware vSphere虚拟机备份。</p>
<hr>
<p>数据的灾备是很重要的，就像是开车系安全带，骑小摩托戴头盔，都是为了安全。在这篇博客中，将带着大家一起看看在利用群晖NAS备份vsphere中的虚拟机。</p>]]>
    </summary>
    <title>使用群晖NAS备份vSphere虚拟机</title>
    <updated>2021-10-19T13:17:19.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="DevOps" scheme="https://www.realks.com/categories/DevOps/"/>
    <category term="DevOps" scheme="https://www.realks.com/tags/DevOps/"/>
    <content>
      <![CDATA[<p>DevOps是什么？在不同的组织会给出不同的解释，目前也没有一个通用的定义。总结一下，如下。</p><p>DevOps 是一系列<strong>文化理念</strong>、<strong>实践</strong>和<strong>工具</strong>的集合。其目的是：</p><ol><li>提高组织<strong>高速的可靠的交付能力。</strong></li><li>提升组织内部<strong>沟通和协作。敏捷软件开发实践</strong>打破了BA（需求分析）、QA（测试）和Dev（开发）之间的“墙”，使得三者之间信息互通，对于同一个需求理解是一致的。DevOps则是打破了Dev(开发)和Ops之间的“墙”，使得软件开发、部署、维护之间形成一条流动的通道。</li></ol><span id="more"></span><h2 id="文化理念"><a href="#文化理念" class="headerlink" title="文化理念"></a>文化理念</h2><p>DevOps文化的核心目的打破团队（Dev和Ops）之间的“墙”，提高团队之间的透明度、沟通和协作。DevOps的核心理念可以理解为如下四点：</p><ol><li>共同承担责任（shared responsibilities）</li><li>反馈（Feedback）</li><li>自动化（Automation）</li><li>质量内建（Build quality in）</li></ol><h3 id="共同承担责任（shared-responsibilities）"><a href="#共同承担责任（shared-responsibilities）" class="headerlink" title="共同承担责任（shared responsibilities）"></a>共同承担责任（shared responsibilities）</h3><p>狭义上，Dev和Ops应当共同对产品的成败负责。在敏捷软件开发的实践前提下，团队中的所有角色（包括但不限于BA、Dev、QA、Ops）共同对产品的成败负责。在整个产品的生命周期内，团队应当共同承担维护系统的责任，确保可以更快<strong>可靠地交付产品。</strong></p><h3 id="反馈（Feedback）"><a href="#反馈（Feedback）" class="headerlink" title="反馈（Feedback）"></a>反馈（Feedback）</h3><p>反馈对于DevOps是非常重要的。通过反馈，我们可以不断地改进团队不同角色之间的协同工作方式以及系统。</p><p>反馈不仅仅包括团队角色之间的反馈，还应该包含系统与团队的反馈。比如：</p><ol><li>pipeline状态，应当及时地通知到团队，推动团队下一步的工作。</li><li>生产事故，监控到生产事故应当及时反馈给团队。当API Gateway短时间内出现大量5xx错误，我们应当及时处理，提高用户的满意度。</li><li>根据生产监控的各种指标来优化系统。</li></ol><p>在DevOps的实践过程中，我们要将反馈这个核心理念铭记于心。在增强反馈的过程中，也要注意不要过度，应当避免团队成员陷入过多工具和消息中。</p><h3 id="自动化（Automation）"><a href="#自动化（Automation）" class="headerlink" title="自动化（Automation）"></a>自动化（Automation）</h3><p>自动化是整个DevOps的基石，有助于团队协作。自动化测试、自动化部署等可以让团队各种关注于业务本身，减少人为错误的机会。自动化可以让团队更快、更可靠地构建、测试、发布产品。</p><p>在实践DevOps的过程中，我们应当尽可能地减少手动操作。实现自动化的过程中，我们需要不同角色之间的协作和沟通。建议有一套统一的自动化脚本来增强团队不同角色之间的协作，建立统一的上下文。</p><h3 id="质量内建（Build-quality-in）"><a href="#质量内建（Build-quality-in）" class="headerlink" title="质量内建（Build quality in）"></a>质量内建（Build quality in）</h3><p>质在开发过程中，要求软件生命周期之间参与的各个角色都需要实时的对软件的质量负责。确保软件在交付到下一环节前已经有了基础的质量保证。从而减少因为质量问题导致的返工，避免浪费大量人力成本。</p><p>为更快更可靠地交付应用和服务，在源头上对质量进行把控是必不可少的。</p><h2 id="实践"><a href="#实践" class="headerlink" title="实践"></a>实践</h2><p>DevOps的常见实践如下：</p><ol><li><a href="/2021/04/24/what-is-ci-cd/">CI&#x2F;CD</a>： 持续集成、持续交付或持续部署。CI&#x2F;CD是整个DevOps的基础，pipeline就是CI&#x2F;CD的具象化。</li><li>持续测试（Continuous testing）</li><li>持续监控（Continuous Monitoring）</li><li>基础设施既代码（Infrastructure as Code，IaC）</li></ol><p>DevOps实践的具体解释，不在此文中赘述，为有另外的文章单独解释。</p><h2 id="常见工具"><a href="#常见工具" class="headerlink" title="常见工具"></a>常见工具</h2><p>协作工具：Jira, Mural, Slack, Microsoft Teams等</p><p>版本控制工具：Github, Gitlab, Bitbucket等</p><p>持续集成工具：Jenkins, TeamCity, Travis CI等</p><p>部署工具：Terraform, AWS CloudFormation, Ansible, Helm等</p><p>监控工具：Zabbix， AWS CloudWatch等</p><p>DevOps的工具极其种类非常多，上述不过是其一小部分。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ol><li><a href="%5Bhttps://en.wikipedia.org/wiki/DevOps%5D(https://en.wikipedia.org/wiki/DevOps)">DevOps</a></li><li><a href="%5Bhttps://aws.amazon.com/devops/what-is-devops/%5D(https://aws.amazon.com/devops/what-is-devops/)">AWS-What is DevOps</a></li><li><a href="%5Bhttps://www.atlassian.com/devops%5D(https://www.atlassian.com/devops)">Atlassian - DevOps</a></li><li><a href="%5Bhttps://martinfowler.com/bliki/DevOpsCulture.html%5D(https://martinfowler.com/bliki/DevOpsCulture.html)">Martin Fowler- DevOps Culture</a></li></ol>]]>
    </content>
    <id>https://www.realks.com/2021/10/18/what-is-devops/</id>
    <link href="https://www.realks.com/2021/10/18/what-is-devops/"/>
    <published>2021-10-18T14:41:45.000Z</published>
    <summary>
      <![CDATA[<p>DevOps是什么？在不同的组织会给出不同的解释，目前也没有一个通用的定义。总结一下，如下。</p>
<p>DevOps 是一系列<strong>文化理念</strong>、<strong>实践</strong>和<strong>工具</strong>的集合。其目的是：</p>
<ol>
<li>提高组织<strong>高速的可靠的交付能力。</strong></li>
<li>提升组织内部<strong>沟通和协作。敏捷软件开发实践</strong>打破了BA（需求分析）、QA（测试）和Dev（开发）之间的“墙”，使得三者之间信息互通，对于同一个需求理解是一致的。DevOps则是打破了Dev(开发)和Ops之间的“墙”，使得软件开发、部署、维护之间形成一条流动的通道。</li>
</ol>]]>
    </summary>
    <title>什么是DevOps</title>
    <updated>2021-10-18T14:41:45.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="Homelab" scheme="https://www.realks.com/categories/Homelab/"/>
    <category term="Homelab" scheme="https://www.realks.com/tags/Homelab/"/>
    <category term="小米" scheme="https://www.realks.com/tags/%E5%B0%8F%E7%B1%B3/"/>
    <category term="智能家居" scheme="https://www.realks.com/tags/%E6%99%BA%E8%83%BD%E5%AE%B6%E5%B1%85/"/>
    <content>
      <![CDATA[<p>很早就想写一篇这样东西来分享一下自己简单的家庭网络，但是一直懒得写。现在(2021-06-20 03:20:00)有点失眠,就开个头,不知道啥时候能写完,随缘吧。写到后面（2021年6月21日２２：００），果然写偏了，干脆改成小米智能家居体验了。</p><span id="more"></span><h2 id="场景＆体验"><a href="#场景＆体验" class="headerlink" title="场景＆体验"></a>场景＆体验</h2><p>租住的地方是一个一室一厅带厨房和阳台的回迁房，这也基本上就是我的活动范围。白天不是在沙发上窝着，就是书桌前坐着，晚上当然是躺床上了。</p><p>先从最简单的地方说起，卧室。卧室里面最主要的两个东西，空调和床。这个空调是一个我之前从未听说过的品牌—志高，当然也有点老旧了，也自然是不可能联网的。成都的夏天还是比较热的，在客厅书桌前干活的我感觉到了热了之后，需要回到卧室找到遥控器打开空调，略微有点麻烦，也容易打断自己的思路。</p><p>这个时候就需要引入一个设备—<strong>米家空调伴侣2</strong>。这个小东西可以将普通空调智能化，说人话，就是遥控器可以做的事情它都可以做，还不需要你在它面前操作。你把它插在空调插座上，再把空调插头插在它身上，让它蹭上你家WiFi，然后就可以通过<strong>米家APP</strong>操作空调了。这是第一步，我们可以把遥控器扔到某个不用的角落里面了。</p><p>在炎热的夏天，我在电脑前面敲着键盘，码着字，机柜里面几台服务器呼呼地跑着，温度逐渐上升，感受到了温度的残忍，于是我拿出手机，通过<strong>米家APP</strong>将空调打开并调至１６度，慢慢感觉到了一丝凉爽。简而言之，当我坐在书桌前且周围的温度让我感到炎热时，开启空调，调整为制冷模式，设定温度为16度。如果我知道具体温度是多少，我就可以将“<strong>让我感到炎热时</strong>”量化为一个具体数值。因此我们需要一个传感器来测量温度－－－<strong>小米温湿度传感器</strong>。再来一个<strong>小米人体传感器</strong>来检测是否有人移动，这个比较鸡肋，一分钟检测一次，假设第一分钟检测到了你，然后你保持不动，那么后续就不会检测到有人移动。然后在米家APP中添加一条智能：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">同时满足　小米人体传感器有人移动　和　小米温湿度传感器温度高于30度时　执行开启空调到指定状态</span><br></pre></td></tr></table></figure><p>添加之后需要一段时间之后才会生效，大概几分钟吧，没有具体测试。当你感受到炎热时，站起来再坐下就可以触发上述规则了。有点智障了。。。。人体传感器就不能判断有没有人吗？</p><p>有一段时间，尝试着不把手机带入卧室，这个时候就需要一个闹钟叫醒我起床，还有如果半夜醒来了看个时间。小爱触屏音响就完美的满足了我的需求，在工作日和非工作日可以在不同的时间叫醒我。懒到极致的我，在之前总是被迫起床关灯以及抹黑找床（经常撞伤），因为卧室只有一个开关且在门口，直到有一天看到<strong>米家床头灯２代</strong>，及时购入。之后的生活就是进卧室时，“小爱同学，开灯”，躺下睡觉时，“小爱同学，关灯”。</p><p>一朝被盗，终身防贼。２０１8年９月７日凌晨（具体我也不知道），在我回成都的第三个月，我家被入室盗窃了，当天报案，至今没有结果，估摸着没有结果了。当天入手了门窗传感器，人体传感器，小米摄像头等，所有有可能的进入的家里的窗户和门都安装了门窗传感器，阳台安装了小米摄像头和红外传感器。同时小米摄像头的数据也会传输到NAS中，多NAS备份，异地备份。租房的一个小Tips，不要租低楼层的，临街的以及外面有可能进入的。</p><p>一个人住总是怕钥匙忘带或者不小心把钥匙丢了，没有备份就没有安全感。<strong>小米智能门锁</strong>完美解决了这个问题。并且在我回家的时候，可以自动触发关闭摄像头并开启<strong>小米网关</strong>在家模式。</p><h2 id="痛点"><a href="#痛点" class="headerlink" title="痛点"></a>痛点</h2><p>使用过程中，有哪些痛点？</p><p>１.　我会经常更换自己家里的WiFi密码，看心情可能一周一换，也可能一月一换。在更新的过程中，需要把这些智能家居设备重新联网，工作量巨大。</p><p>２.　上述的那个问题，小米给出了解决方案，使用小米路由器的一个新功能畅快连。经过本人试验，并没有什么用途，因为我家所有的设备的最新固件都不支持。并且小米路由器对于我不适用，重度网络使用者很容易就感受到了断流的痛。</p><p>３.　米家APP在设备多的时候，极其难用。没有一个很好的控制中心。未来打算尝试一下Home Assistant，自己收集数据，自己做dashboard，不让数据离开局域网。</p><p>４.　如果你的小米网关坏掉了，那我只能表示恭喜你了。你需要重新添加设备，然后重新命名极其麻烦的过程。</p><p>５.　核心模块（比如小米网关）不是高可用的。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>在这个野蛮的互联网时代，谁又不是在裸奔呢？</p><p>如果不是一个公众人物，选择一个某个智能家居平台的时候，只需要问自己信任与否。在使用的过程中，尽量减少对云存储的使用，在家的时候尽量关闭摄像头等等。没有什么事比健康安全地活着更重要。</p><p>如果你是一个公众人物，能不用就不用，攻击你的收益远远高于成本。</p>]]>
    </content>
    <id>https://www.realks.com/2021/06/21/homelab-xiaomi-smarthome/</id>
    <link href="https://www.realks.com/2021/06/21/homelab-xiaomi-smarthome/"/>
    <published>2021-06-21T14:00:00.000Z</published>
    <summary>
      <![CDATA[<p>很早就想写一篇这样东西来分享一下自己简单的家庭网络，但是一直懒得写。现在(2021-06-20 03:20:00)有点失眠,就开个头,不知道啥时候能写完,随缘吧。写到后面（2021年6月21日２２：００），果然写偏了，干脆改成小米智能家居体验了。</p>]]>
    </summary>
    <title>小米智能家居体验</title>
    <updated>2021-06-21T14:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>ChengQing</name>
    </author>
    <category term="DevOps" scheme="https://www.realks.com/categories/DevOps/"/>
    <category term="DevOps" scheme="https://www.realks.com/tags/DevOps/"/>
    <category term="Best Practices" scheme="https://www.realks.com/tags/Best-Practices/"/>
    <category term="实践" scheme="https://www.realks.com/tags/%E5%AE%9E%E8%B7%B5/"/>
    <category term="零手动操作" scheme="https://www.realks.com/tags/%E9%9B%B6%E6%89%8B%E5%8A%A8%E6%93%8D%E4%BD%9C/"/>
    <category term="童子军军规" scheme="https://www.realks.com/tags/%E7%AB%A5%E5%AD%90%E5%86%9B%E5%86%9B%E8%A7%84/"/>
    <category term="奥卡姆剃刀" scheme="https://www.realks.com/tags/%E5%A5%A5%E5%8D%A1%E5%A7%86%E5%89%83%E5%88%80/"/>
    <content>
      <![CDATA[<p>这又是一番偏执的胡言乱语。做DevOps两年以来，见过好的实践，也见过糟糕的实践。每一个对DevOps实践都有不同的认识，在这篇博客，我只是聊聊自己的观点，如有不对敬请指正。</p><p>大致而言，我会遵循三个原则：</p><ol><li>零手动操作。</li><li>干净：童子军军规。</li><li>简单：奥卡姆剃刀。</li></ol><span id="more"></span><h2 id="零手动操作"><a href="#零手动操作" class="headerlink" title="零手动操作"></a>零手动操作</h2><p>什么是手动操作？任何没有被代码化（as code）的资源或者操作都可以被认为是手动操作。</p><p>常见的手动操作：</p><ul><li>基础设施没有代码化，存在资源是没有被代码管理，或者部分操作没有被管理。</li><li>应用部署流程需要手动干预。</li><li>应用配置需要手动指定等。</li></ul><h3 id="真的要手动操作？"><a href="#真的要手动操作？" class="headerlink" title="真的要手动操作？"></a>真的要手动操作？</h3><p>当你需要做任何手动操作之前，先问自己一系列问题：</p><ol><li>该操作是否以及被代码化（as code），如果是，那就执行代码。不要把自己的代码遗落在角落里。如果没有，继续下一个问题。</li><li>该操作是否紧急？如果紧急，那么可以临时手动操作，但是需要多个人一起以及严格的流程。之后，需要将该操作代码化。如果你不想代码化，那么看看下面一系列问题？</li><li>该操作在后续是否还有发生的机率？如果有，就需要代码化。如果你依然不愿意，继续下一个问题。</li><li>如何确保在下一次执行（可能间隔一周，也可能是间隔一个月）的时候完全重复当前操作？任何手动操作都是不可被追溯的，不可完全被重复的。如果确保不了，那就需要代码化。如果能确保，那你就是神，我只能表示膜拜。</li><li>我们有完备的文档，还需要代码化吗？代码比文档可靠，优雅的代码可读性远高于文档。</li></ol><h3 id="避免手动操作的实践"><a href="#避免手动操作的实践" class="headerlink" title="避免手动操作的实践"></a>避免手动操作的实践</h3><ol><li>基础设施即代码（IaC，Infrastructure as code）</li><li>CI&#x2F;CD</li><li>自动化脚本（Automation script）</li><li>GitOps</li></ol><h2 id="干净：童子军军规"><a href="#干净：童子军军规" class="headerlink" title="干净：童子军军规"></a>干净：童子军军规</h2><blockquote><p>The Boy Scout Rule: Always leave the campground cleaner than you found it.</p></blockquote><p>干净，可以从两方面说，<strong>代码整洁</strong>和<strong>环境干净</strong>。童子军军规：让营地比你来时更干净。这是一条非常适用的规则。</p><h3 id="代码整洁"><a href="#代码整洁" class="headerlink" title="代码整洁"></a><strong>代码整洁</strong></h3><p>代码整洁中的“代码”不仅仅指业务代码，还应当包含测试代码，基础设施代码等一切代码。我见过一些业务代码很整洁，非业务代码很糟糕的项目，为什么会这样呢？我大胆地猜测一下，团队没有把基础设施代码当成代码，而是在战略上忽视了它。</p><p>所有的代码享有平等的地位。无论是否是业务代码，在应用的生命周期中，我们都需要去维护。</p><p>关于这部分的内容，<strong>强烈推荐学习一下《代码整洁之道》和《重构》</strong>。</p><h3 id="环境干净"><a href="#环境干净" class="headerlink" title="环境干净"></a>环境干净</h3><p>在开发阶段，保持开发环境的干净。</p><p>在CI&#x2F;CD 环境中，保持构建、测试、部署环境干净。通常而言，一个CI&#x2F;CD 的Agent会被不止一个应用使用，因此保持环境的干净就显得尤为重要。</p><p>在生产环境中，保持运行环境干净。</p><h3 id="实践"><a href="#实践" class="headerlink" title="实践"></a>实践</h3><ol><li>TDD</li><li>Simple Design</li><li>容器化</li><li>Code Diff</li></ol><h2 id="简单：奥卡姆剃刀原理"><a href="#简单：奥卡姆剃刀原理" class="headerlink" title="简单：奥卡姆剃刀原理"></a>简单：奥卡姆剃刀原理</h2><blockquote><p>Entities should not be multiplied unnecessarily</p></blockquote><p>在写这篇的博客的时候，我也一直在纠结是不是要把干净和简单合并在一起写。在提到干净的时候，它和脏是对立的，往往是指代码是否整洁。而简单和复杂是相对应的，通常是和业务，架构，技术栈等相关的。</p><p>业务复杂与否是来自于功能需求，这一块儿没有深入研究过，暂且不谈。</p><p>架构和技术栈的复杂度来自于非功能性需求。一个项目总会有人员的变动，在人员变动的过程中，复杂的东西在传递的过程中很容易失真。在引入一项新的技术或者语言时，需要仔细考虑是否真的需要，如果不是必须的就不要引入。简而言之，遵循奥卡姆剃刀原理：如无必要，勿增实体(Entities should not be multiplied unnecessarily)。</p>]]>
    </content>
    <id>https://www.realks.com/2021/05/31/some-devops-practices/</id>
    <link href="https://www.realks.com/2021/05/31/some-devops-practices/"/>
    <published>2021-05-31T11:54:58.000Z</published>
    <summary>
      <![CDATA[<p>这又是一番偏执的胡言乱语。做DevOps两年以来，见过好的实践，也见过糟糕的实践。每一个对DevOps实践都有不同的认识，在这篇博客，我只是聊聊自己的观点，如有不对敬请指正。</p>
<p>大致而言，我会遵循三个原则：</p>
<ol>
<li>零手动操作。</li>
<li>干净：童子军军规。</li>
<li>简单：奥卡姆剃刀。</li>
</ol>]]>
    </summary>
    <title>DevOps的实践经验</title>
    <updated>2021-05-31T11:54:58.000Z</updated>
  </entry>
</feed>
