Add "How to Run AI Agents on Kubernetes with Pulumi" blog post - #20550
Conversation
New evergreen blog post (authors: joe-duffy) covering how to provision and govern Kubernetes infrastructure for agentic AI workloads with Pulumi: what changes about K8s infra when the workload is an agent, a fair survey of the agentic-Kubernetes ecosystem (kagent, KServe, Kueue, KubeRay, vLLM/llm-d, DRA, Gateway API Inference Extension, NVIDIA GPU Operator), a TypeScript + Python walkthrough for provisioning agent infra, why general-purpose languages help as configuration branching grows, a governance section (Pulumi Policies, ESC, RBAC, and a precise Insights vs. Neo distinction), where Pulumi Neo fits, and a 6-question FAQ. Also fixes two pre-existing bugs found while building a local schema render harness to verify this post's structured data: - layouts/partials/schema/collectors/howto-entity.html: `trim` was being piped as `$stepText | ... | trim " \t\n\r"`, which passes the trim cutset as the piped value and the accumulated step text as an unused argument, so every HowTo step's `text` field silently rendered as just the whitespace cutset instead of the actual step content across all three step-emission call sites. Fixed to call `trim` directly with the step text as first argument. Also fixed a related bug in the same file where hitting an empty line right after the last numbered-list item could get that final step appended a second time when a later heading was reached, because $stepText/$inNumberedList were only reset on a header line, not on the blank line that (correctly) triggers the first save. - layouts/partials/schema/graph-builder.html: added an additive `howto_schema: true` frontmatter gate for blog posts, so a how-to-style blog post can carry a HowTo entity alongside its normal BlogPosting entity (author, date, publisher) rather than only being able to fully replace BlogPosting via the existing `schema_type: howto` gate. Mirrors the existing `faq_schema` opt-in pattern; guarded so it never fires alongside `schema_type: howto` to avoid emitting HowTo twice. Verified both fixes and the new post's JSON-LD (BlogPosting + FAQPage + HowTo all present, 12 FAQ entries, 6 clean HowTo steps) by copying the schema partials into a disposable local Hugo site and inspecting the rendered output; harness was not committed. Per Google's 2026-05-07 deprecation of FAQPage rich results (following HowTo's 2023 deprecation), this markup is now hygiene-level machine- readable structure rather than a rich-result or proven AI-citation lever; noted plainly in the PR description rather than oversold.
5de901b to
1dc5541
Compare
Social Media Reviewcontent/blog/ai-agents-on-kubernetes/index.mdX — PASSLinkedIn — PASSBluesky — PASSSuggestions (advisory)These are stylistic notes — they don't block the post. X
Bluesky
Updated for commit |
|
Your site preview for commit 1d969c5 is ready! 🎉 http://www-testing-pulumi-docs-origin-pr-20550-1d969c58.s3-website.us-west-2.amazonaws.com Changed pages: |
Lighthouse Performance ReportCommit: 1d969c5 | Metric definitions
|
|
@claude #new-review — this PR was opened while conflicted against a stale base (10 days old), so the initial triage/review never ran (zero pull_request workflow runs fired at all until this fix). Rebased onto current master, resolved the single conflict in |
Pre-merge Review — Last updated 2026-07-29T01:25:00ZTip Summary: This PR adds a new long-form blog post — How to Run AI Agents on Kubernetes with Pulumi — plus two Hugo schema-template changes that let a blog post emit Review confidence:
Investigation log
🔍 Verification trail73 claims extracted · 39 verified · 6 unverifiable · 3 contradicted
📊 Editorial balanceSection depth, mention distribution, recommendation steering
🚨 Outstanding in this PRNo outstanding findings in this PR.
|
|
🤖 Review regenerated on @joeduffy's request. |
…ften unsupported kagent superlative, split Bluesky copy into two paragraphs
|
Thanks for the review — fixed both outstanding findings plus the Bluesky social-copy issue the social-review bot flagged separately:
Grepped the whole file for "September 2025" and "most Kubernetes-native" afterward — no other occurrences of either issue. The step-4-of-6 code walkthrough note and the unverifiable-claims list are accurate as flagged; I left those as-is since they're editorial/low-confidence notes rather than factual errors, but happy to expand the walkthrough if you'd like the last two steps inlined as code rather than prose. @joeduffy this is ready for your review/approval whenever you have a moment — @claude #update-review |
|
🤖 Review updated on @joeduffy's request. |
The 66% figure and the CNCF 2026 attribution were both correct, but the
framing around them overstated the finding in three ways: the denominator
widened from organizations already hosting generative AI models to
organizations generally, present-tense usage became future intent ("plan
to run" / "betting on"), and "some or all of their inference workloads"
became "their generative AI workloads."
CNCF's actual wording: "66% of organizations hosting generative AI models
use Kubernetes to manage some or all of their inference workloads."
Corrected in the opening paragraph and in the X and LinkedIn social copy,
which auto-post on merge. Bluesky doesn't cite the statistic. X copy is
236 chars, down from 243, so it stays inside the limit the earlier trim
established.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
📣 Social media posts are live for
|
…p escalation, SHA-based review freshness Gap 1 — no verdict for "value correct, meaning drifted" (PR pulumi#20550: three stacked framing distortions on an accurate 66% CNCF figure rendered as verified, with the drift observation buried in evidence prose): - New `framing-drift` (🌀) verdict across verify-claims.py, compose-review.py, validate-pinned.py (schema v18→v19), splicer.py, validator-fix.py, record-claims.py (kept), reverify-claims.py (stale), compose-pr-body.py. - Structured `framing` enum on verify_claim (exact-match / entailed-narrower / overclaim-broader / shifted / none); _finalize_verdict deterministically coerces a verified verdict carrying a drift shape to framing-drift, so the observation can no longer soft-pedal into verified prose. - Framing taxonomy recast in entailment terms ("does the source, as quoted, prove the claim as written?") and reconciled across claim-extraction.md, fact-check.md (whose fallback section previously mapped the same labels to opposite verdicts), and the verifier prompt. - Bucket:⚠️ Low-confidence by default; reviewer promotes to 🚨 when the drifted phrasing rides social.* frontmatter (auto-posted on merge). - New source-discipline rule: the content under review never verifies its own technical claims (the AES256-GCM circular-verification case on PR pulumi#20371). Gap 2 — pass1 turn-cap and routing failures reported as unverifiable (PR pulumi#20556: six REST-API claims died at the 8-turn cap; PR pulumi#20371: a pure CLI-behavior claim routed to web search, which cannot read product source): - pass1 turn cap 8→12; cap exhaustion now auto-escalates once to pass3 instead of returning bare unverifiable. - Symmetric route escalation: pass3 verifiers may emit route_escalation: "pass1" for Pulumi-product-behavior claims that carry no pulumi-shaped token (one hop, ping-pong guarded). - Terminal turn-cap records carry `turn_cap_exhausted: true` and retryable phrasing; compose-review stubs them with an explicit "never call this out of scope" TODO so budget failures and architectural limits can no longer render identically downstream. Gap 3 — stale pinned review kept review:no-blockers (PR pulumi#20556: a Copilot conflict-resolution push fired no pull_request:synchronize event, so mark-stale never ran; App pushes like workprentice[bot] do fire it — the suppressed class is Copilot-agent and GITHUB_TOKEN pushes): - compose-review.py stamps a machine-readable `<!-- CLAUDE_REVIEW_HEAD <sha> -->` sentinel under the review header (the workflow already passes --head-sha); update.md requires the re-entrant path to refresh it. - pr-review SKILL Step 2 now runs a SHA freshness check before trusting CURRENT labels, with a review-history fallback for pre-sentinel reviews. - New scheduled review-label-reconcile.yml workflow compares each labeled open PR's reviewed SHA against its head and applies the same stale transition mark-stale would have applied — the polling-side net for pushes that suppress webhook events. Tests: new test_framing_drift.py (36 checks: constant sync, coercion, escalation hops, cap marker, stub placement, validator acceptance/violation directions, header sentinel); all existing docs-review and content-review suites pass; make lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcNQVWHUyvbbe5Go9MGxwA
…p escalation, SHA-based review freshness Gap 1 — no verdict for "value correct, meaning drifted" (PR #20550: three stacked framing distortions on an accurate 66% CNCF figure rendered as verified, with the drift observation buried in evidence prose): - New `framing-drift` (🌀) verdict across verify-claims.py, compose-review.py, validate-pinned.py (schema v18→v19), splicer.py, validator-fix.py, record-claims.py (kept), reverify-claims.py (stale), compose-pr-body.py. - Structured `framing` enum on verify_claim (exact-match / entailed-narrower / overclaim-broader / shifted / none); _finalize_verdict deterministically coerces a verified verdict carrying a drift shape to framing-drift, so the observation can no longer soft-pedal into verified prose. - Framing taxonomy recast in entailment terms ("does the source, as quoted, prove the claim as written?") and reconciled across claim-extraction.md, fact-check.md (whose fallback section previously mapped the same labels to opposite verdicts), and the verifier prompt. - Bucket:⚠️ Low-confidence by default; reviewer promotes to 🚨 when the drifted phrasing rides social.* frontmatter (auto-posted on merge). - New source-discipline rule: the content under review never verifies its own technical claims (the AES256-GCM circular-verification case on PR #20371). Gap 2 — pass1 turn-cap and routing failures reported as unverifiable (PR #20556: six REST-API claims died at the 8-turn cap; PR #20371: a pure CLI-behavior claim routed to web search, which cannot read product source): - pass1 turn cap 8→12; cap exhaustion now auto-escalates once to pass3 instead of returning bare unverifiable. - Symmetric route escalation: pass3 verifiers may emit route_escalation: "pass1" for Pulumi-product-behavior claims that carry no pulumi-shaped token (one hop, ping-pong guarded). - Terminal turn-cap records carry `turn_cap_exhausted: true` and retryable phrasing; compose-review stubs them with an explicit "never call this out of scope" TODO so budget failures and architectural limits can no longer render identically downstream. Gap 3 — stale pinned review kept review:no-blockers (PR #20556: a Copilot conflict-resolution push fired no pull_request:synchronize event, so mark-stale never ran; App pushes like workprentice[bot] do fire it — the suppressed class is Copilot-agent and GITHUB_TOKEN pushes): - compose-review.py stamps a machine-readable `<!-- CLAUDE_REVIEW_HEAD <sha> -->` sentinel under the review header (the workflow already passes --head-sha); update.md requires the re-entrant path to refresh it. - pr-review SKILL Step 2 now runs a SHA freshness check before trusting CURRENT labels, with a review-history fallback for pre-sentinel reviews. - New scheduled review-label-reconcile.yml workflow compares each labeled open PR's reviewed SHA against its head and applies the same stale transition mark-stale would have applied — the polling-side net for pushes that suppress webhook events. Tests: new test_framing_drift.py (36 checks: constant sync, coercion, escalation hops, cap marker, stub placement, validator acceptance/violation directions, header sentinel); all existing docs-review and content-review suites pass; make lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcNQVWHUyvbbe5Go9MGxwA
…p escalation, SHA-based review freshness (#20577) * Close three docs-review pipeline gaps: framing-drift verdict, turn-cap escalation, SHA-based review freshness Gap 1 — no verdict for "value correct, meaning drifted" (PR #20550: three stacked framing distortions on an accurate 66% CNCF figure rendered as verified, with the drift observation buried in evidence prose): - New `framing-drift` (🌀) verdict across verify-claims.py, compose-review.py, validate-pinned.py (schema v18→v19), splicer.py, validator-fix.py, record-claims.py (kept), reverify-claims.py (stale), compose-pr-body.py. - Structured `framing` enum on verify_claim (exact-match / entailed-narrower / overclaim-broader / shifted / none); _finalize_verdict deterministically coerces a verified verdict carrying a drift shape to framing-drift, so the observation can no longer soft-pedal into verified prose. - Framing taxonomy recast in entailment terms ("does the source, as quoted, prove the claim as written?") and reconciled across claim-extraction.md, fact-check.md (whose fallback section previously mapped the same labels to opposite verdicts), and the verifier prompt. - Bucket:⚠️ Low-confidence by default; reviewer promotes to 🚨 when the drifted phrasing rides social.* frontmatter (auto-posted on merge). - New source-discipline rule: the content under review never verifies its own technical claims (the AES256-GCM circular-verification case on PR #20371). Gap 2 — pass1 turn-cap and routing failures reported as unverifiable (PR #20556: six REST-API claims died at the 8-turn cap; PR #20371: a pure CLI-behavior claim routed to web search, which cannot read product source): - pass1 turn cap 8→12; cap exhaustion now auto-escalates once to pass3 instead of returning bare unverifiable. - Symmetric route escalation: pass3 verifiers may emit route_escalation: "pass1" for Pulumi-product-behavior claims that carry no pulumi-shaped token (one hop, ping-pong guarded). - Terminal turn-cap records carry `turn_cap_exhausted: true` and retryable phrasing; compose-review stubs them with an explicit "never call this out of scope" TODO so budget failures and architectural limits can no longer render identically downstream. Gap 3 — stale pinned review kept review:no-blockers (PR #20556: a Copilot conflict-resolution push fired no pull_request:synchronize event, so mark-stale never ran; App pushes like workprentice[bot] do fire it — the suppressed class is Copilot-agent and GITHUB_TOKEN pushes): - compose-review.py stamps a machine-readable `<!-- CLAUDE_REVIEW_HEAD <sha> -->` sentinel under the review header (the workflow already passes --head-sha); update.md requires the re-entrant path to refresh it. - pr-review SKILL Step 2 now runs a SHA freshness check before trusting CURRENT labels, with a review-history fallback for pre-sentinel reviews. - New scheduled review-label-reconcile.yml workflow compares each labeled open PR's reviewed SHA against its head and applies the same stale transition mark-stale would have applied — the polling-side net for pushes that suppress webhook events. Tests: new test_framing_drift.py (36 checks: constant sync, coercion, escalation hops, cap marker, stub placement, validator acceptance/violation directions, header sentinel); all existing docs-review and content-review suites pass; make lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KcNQVWHUyvbbe5Go9MGxwA * Count framing-drift separately from contradicted in the headline tallies The first cut folded framing-drift into CONTRADICTION_FAMILY, which feeds the 🔍 trail <details> header and the investigation log's parenthetical — both of which label that number `contradicted`. But framing-drift defaults to⚠️ , not 🚨, so the headline reported contradictions the body didn't contain: a fork run produced `2 verified · 0 unverifiable · 2 contradicted` above a trail with zero ❌ lines, and another read `4 contradicted` for 1 real ❌ plus 3 🌀. CONTRADICTION_FAMILY now means what its name says (contradicted + mismatch) and drift gets its own `· W framing-drift` segment in both places, omitted entirely when the count is zero so ordinary reviews are byte-identical. The per-lane Pass 2/3 V/C/U triple keeps drift in the C column — that triple's shape is pinned by validate-pinned.py's PASS2_OUTCOME_RE/PASS3_OUTCOME_RE, and it's a routing diagnostic rather than a reader-facing count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
What
Adds a new evergreen blog post, "How to Run AI Agents on Kubernetes with Pulumi" (
content/blog/ai-agents-on-kubernetes/index.md, authors: joe-duffy), and fixes two pre-existing bugs in the HowTo schema template found while verifying this post's structured data.Why this post, why now
Kubernetes is where the "agentic AI" conversation is increasingly landing: CNCF's January 2026 annual survey found 66% of organizations are betting on Kubernetes to run generative AI workloads. A real ecosystem has grown up around that (kagent, KServe, Kueue, KubeRay, vLLM/llm-d, Dynamic Resource Allocation, the Gateway API Inference Extension), but most of it assumes the cluster and its governance are already handled. This post covers the gap: what's different about provisioning for an agent workload versus a normal service, a fair and generous survey of that ecosystem (Pulumi's job is to provision and govern underneath these tools, not compete with them), a TypeScript + Python walkthrough for provisioning agent infrastructure with Pulumi, why a general-purpose language earns its keep as configuration branching grows (while conceding Helm/Kustomize are fine for simple, static manifests), a governance section (Pulumi Policies, ESC, RBAC, and a precise Insights-vs-Neo distinction on cost claims), where Pulumi Neo fits, and a 6-question FAQ.
Reviewed by Joe Duffy (via an internal review pass) for voice and technical accuracy; two sentences were revised to speak in first person per that feedback, and the Insights-vs-Neo distinction was confirmed as correct and worth keeping explicit.
Keyword / AEO rationale
Schema: FAQPage + HowTo, and the honest caveat
The post sets
faq_schema: trueand a newhowto_schema: trueflag, both additive to the post's normal BlogPosting entity — i.e. the rendered@graphincludes BlogPosting, FAQPage, and HowTo all at once, not one replacing another.Important context on rich results: Google deprecated FAQPage rich results in Search on 2026-05-07 (API support removed 2026-08), following HowTo's 2023 deprecation. Neither schema type produces a rich result today. I'm including this markup anyway because it's accurate, machine-readable structure that costs nothing and may still help LLM crawlers parse the page's Q&A/step structure, but I want to be explicit in this PR that it is not a proven citation lever — see the discussion in
2026_geo_schema_citation_study_findings— so nobody should read "we added FAQ/HowTo schema" as a growth bet in itself.Template changes (and why they were necessary)
While building a disposable local Hugo harness to verify this post's JSON-LD before pushing (copying
layouts/partials/schema/*into a throwaway site and rendering), I found the existinghowto-entity.htmlwas silently broken for every page that already usesschema_type: howto, not just this new post:HowToStep.textfield rendered as just a whitespace string ("\t\n\r"), never the actual step content. Root cause:{{ $stepText | ... | trim " \t\n\r" }}— Hugo'strimpiped this way receives the piped value as its cutset argument and the accumulated step text is dropped. Fixed at all three step-emission sites to calltrimdirectly with the step text as the first argument.$stepText/$inNumberedListwere only reset on a header line, not on the blank line that (correctly) already triggers the first save. Fixed by unconditionally clearing both after the end-of-step block runs.I verified both fixes render correctly (clean single-sentence step text, no duplicate steps) against this post and a couple of minimal synthetic fixtures before pushing. The harness itself was not committed.
Additionally,
graph-builder.htmlgets a new additivehowto_schema: truegate for blog posts (mirrors the existingfaq_schemaopt-in pattern), guarded to never fire alongsideschema_type: howtoso the two mechanisms can't both emit a HowTo entity for the same page. This is template-level leverage: any future how-to-shaped blog post can now carry BlogPosting + HowTo + FAQPage together, rather than needing to give up its BlogPosting entity (author, date, publisher) to get HowTo, or a per-page hack.Fact-check table
Agent,ModelConfig,ModelProviderConfig,RemoteMCPServer,SandboxAgent), v0.9.9 current releasego/api/v1alpha2/), https://kagent.dev/docs/kagent/introduction/installationkubernetes.helm.v4.Chart,kubernetes.Provider,kubernetes.apiextensions.CustomResource,kubernetes.yaml.v2.ConfigFile/ConfigGroupI did not independently verify the exact
kagent.dev/v1alpha2API group string (only the CRD kinds and version path, via the live GitHub source) — the post includes an explicit caveat telling readers to check kagent's own CRD manifests before shipping the sample to production.🧠 This PR was created by workprentice on behalf of @joeduffy.