Pulumi Infrastructure
SkillFiles & storageEsposter Pulumi infrastructure conventions for apps/infra — the package shape and generated ctix barrel, one resource per file under src/<provider>/resources/<ProviderNamespace>/<resourceTypes>/ with camelCase names matching the export, protect on imported resources, the parent every new resource must set, resource outputs over duplicated identifier constants and when a named constant is earned, per-stack files with shared environment-independent constants, Output<string> vs plain string in template literals and object keys, namespace provider imports (never named), the unconditional alias ban, generated output safety, the security-hardening blockers, observability deliberately off for cost, and where infra docs live — plus deep dives on the preview/up ritual and provider bumps, rename/re-parent/import migrations, and Azure Native + GitHub provider quirks. Apply when modifying apps/infra.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Pulumi Infrastructure skill
What this skill tells your AI
The instructions your AI receives, as published by esposter/esposter in .agents/skills/pulumi-infra/SKILL.md and read by ahel’s review.
Apply when modifying apps/infra.
Package Shape
apps/infrais a private Pulumi package managing Azure (and, from v12, GitHub) infrastructure in oneprodstack.Pulumi.yamluses the compileddist/index.jsas the entrypoint.src/is split by provider:src/azure/{resources,constants,services}andsrc/github/…. The generatedsrc/index.tsbarrel stays at the top level and covers every provider.src/index.tsis generated by ctix and gitignored like other package barrels;pnpm buildregenerates it and compiles the program todist/index.js.- Do not hand-edit generated output —
pnpm export:genregeneratessrc/index.ts,pnpm buildregenerates it anddist/together. - Run Pulumi operations only when the user allows Azure/Pulumi access.
Resource Files
- One Azure resource declaration per tracked source file; export exactly one resource constant per file.
- Put declarations under
src/azure/resources/<ProviderNamespace>/<resourceTypes>/mirroring Azure resource IDs after/providers/, e.g.Microsoft.Web/sites,Microsoft.Insights/actionGroups,Microsoft.EventGrid/eventSubscriptions. - File names use camelCase derived from the Azure resource name:
dev-rg-esposter-ae-001→devRgEsposterAe001.ts. The export constant name must match the file name (minus.ts). - Child resources append the Pulumi resource type name as a suffix. Mandatory singleton Azure names like
defaultare omitted (they add no information):devstesposter001Properties.ts,devstesposter001ManagementPolicy.ts. - Keep
protect: trueon imported resources unless the user explicitly asks for a lifecycle change. - Never add
aliasesto any resource — the Pulumi logical name is the Azure resource name here, so an alias can never serve the rename it is suggested for. The recurring review suggestion to add one is closed, never applied. - A deployed identity is renamed like any other identifier. An Azure resource name, a function name, or any string a resource's properties point at (an event subscription's
destinationnaming a function) is corrected in place the moment it is wrong — infra being code is what makes that ordinary rather than a migration (no compatibility debt). Rename,pnpm infra:preview, read the plan. Hesitating on an unpreviewed guess about what a rename would cost is the same false positive as asserting a replacement without one, and it is the more expensive mistake: it leaves the wrong name in place permanently.
Resource Parent Hierarchy
Every new resource must set the parent Pulumi option to the nearest final Azure containment/extension parent:
| Resource category | Correct parent |
|---|---|
| Top-level RG-scoped resources (Logic Apps, API connections, Function Apps, App Service Plans, storage accounts, search services, Web PubSub, Event Grid topics, action groups) | Final resource group (devRgEsposterAe001 / prodRgEsposterAe001) |
| Child/extension of storage account (blob service properties, management policies) | The storage account resource |
| Child/extension of Event Grid topic (event subscriptions) | The Event Grid topic (once it has a final name) |
| Role assignments scoped to a specific managed resource (Logic App website contributor, EventGrid contributor) | That scoped resource |
| Subscription-scoped resources (budgets, policy assignments, subscription-level role assignments) | No parent |
Deferral rule: if the natural parent is still a legacy resource scheduled for rename/deletion, defer setting parent until the migration wave that creates the final parent. Create the child directly under the final parent in that same wave — never under the legacy parent.
Resource References And Dependencies
- Prefer existing Pulumi resource outputs over repeated Azure identifier string literals when one managed resource refers to another. If Pulumi owns the resource, use its output properties (
.name,.id, etc.) as source of truth instead of a separate constant. - Add constants only for values external to managed resources, values required as plain strings in Pulumi options/import IDs, or shared built-in/static identifiers (e.g. role definition IDs).
- A local
constwithin a file is fine when it is the source of truth for that name and reused more than once in the same file (e.g.const workflowName = "dev-logic-esposter-ae-001"used as the Pulumi resource name and the Azure property). Don't introduce a local const that merely duplicates a name owned by another resource file. - Single-use UUIDs inline directly — don't declare
const roleAssignmentName = "uuid"if used once; inline it:roleAssignmentName: "uuid". Applies to any UUID/identifier appearing exactly once. - Named constants only for cross-file reuse — create a constant file only when the value is referenced in ≥2 resource files. External principal IDs not backed by managed resources live in named constants too, under the same ≥2-files rule.
- Per-stack files, shared environment-independent values — dev and prod each keep their own resource file (names, parents, scopes, action groups differ), but any value identical across stacks — KQL alert queries, tags, location, thresholds, repeated literal + explanatory comment pairs — is imported from one shared constant in
src/azure/constants/rather than duplicated per stack. - A value the infra shares with app code interpolates the same constant that code uses (e.g. an advanced-filter prefix comes from the constant the handler filters on), so renaming one cannot leave the infra filter and the code it mirrors silently disagreeing.
- Mixing resource outputs and enum literals in one file is fine — use a resource output (
.name,.id) when Pulumi declares the referenced resource, and the plain enum/constant when it does not. The two forms sitting side by side in onerulesarray is correct, not an inconsistency; don't "fix" one to match the other.
Pulumi Output vs Plain String
resource.name and resource.id are Output<string>, not plain strings:
- As a property value — use dot notation directly; Pulumi accepts
Input<T>(which includesOutput<T>) for all properties:connectionId: conn.id✓ - In a template literal — plain backtick interpolation silently produces
"[object Object]". Usepulumi.interpolate`prefix-${conn.name}-suffix`✓ - As a computed object key —
[conn.name]evaluates to"[object Object]"at runtime (JS calls.toString()eagerly).
Preferred fix for object keys: if the name is used as a key and also in template literals in the same file, define const connectionKey = "the-name" as the local source of truth; use the Output (.id, .name) only for property values inside that keyed object. This avoids pulumi.all().apply() and stays readable. pulumi.all([conn.name]).apply(([name]) => ({ [name]: ... })) is a valid last resort only when there is genuinely no plain-string source of truth.
Provider Imports (namespace, not named)
Always import Pulumi provider packages as a namespace — import * as github from "@pulumi/github", import * as azure_native from "@pulumi/azure-native", import * as pulumi from "@pulumi/pulumi" — and reference members as github.Repository, azure_native.resources.ResourceGroup.
This is a deliberate exception to the repo-wide "prefer named imports from libraries" rule, and the review suggestion to switch a provider to named imports is wrong:
- Provider packages are CommonJS and lazy-load every resource submodule through
utilities.lazyLoad, which installs getters on theexportsobject viaObject.defineProperty. That mechanism only works through the live namespace object fromimport * as. apps/infrais"type": "module", so named ESM imports from those CJS modules force Node's interop to evaluate bindings eagerly —require()-ing every referenced submodule at import time and defeating the lazy-load (slower startup, higher memory). They are not tree-shakable.- Pulumi's own codegen always emits
import * as. Match it; do not "fix" provider imports for lint/style consistency.
Security Constraints
Do not, until the listed app-side migration completes:
- Disable storage shared key access — while app blob clients and SAS generation use connection-string/shared-key auth.
- Disable storage blob public access — while public blob containers in
AzureContainerPropertiesMapare unmigrated. - Disable Azure Search local auth — while
apps/webusesAzureKeyCredentialfor Search. - Disable Event Grid topic local auth — while the main app Event Grid publisher uses
AzureKeyCredential(Azure Functions already useDefaultAzureCredential, but the app path is still key-based). - Restrict Web PubSub to static IP allowlists — while browser clients connect directly from arbitrary public IPs.
- Disable Web PubSub local auth or public REST API access — while app/functions use Web PubSub connection-string service clients over public endpoints.
- Set storage network default action to
Denywithout a complete allowlist, private endpoint, or equivalent migration.
Observability Is Deliberately Off (Cost)
This estate runs with no Application Insights and no Log Analytics — removed from both environments as a deliberate free-tier cost decision, so the Function Apps carry no APPINSIGHTS_* / APPLICATIONINSIGHTS_* app settings. The $0.01 guard budgets (with their StopFunction/DeleteSub action groups + Logic Apps) are the cost ceiling; Azure portal platform metrics answer operator questions for free. Full rationale: apps/web/content/docs/infra/observability.md.
Do not add App Insights, a Log Analytics workspace, diagnostic settings, smart-detector rules, or scheduled-query alerts as an unprompted "observability best practice" — that review suggestion is wrong here and should be closed, not applied, because it reintroduces recurring ingestion cost with no consumer. Only revisit if paid tiers/quotas, an on-call rotation, or a real incident-investigation need make retained telemetry worth the spend. One consequence to design around: nothing can alert off a query over collected traces, so a condition worth noticing — a dead letter quarantined or discarded — is written with context.error and found by inspecting the deadletter container.
Docs
- Durable infrastructure docs live in
apps/infra/docs/, one directory per provider that has any (azure/) with the cross-cutting pages at its root. The convention is the layout, not a list:ls apps/infra/docsnames the current set, and each page's title says what it holds. - The forward roadmap is
apps/web/content/docs/infra/roadmap.md(every item links a proposal inapps/web/content/docs/proposals/infra/); the area index + shipped log isapps/web/content/docs/infra/index.md; phase-2 cost/security findings are inapps/web/content/docs/infra/cost-and-security-posture.md. - Move completed one-off migration notes out of the package once their durable content is represented in
docs/.
Deep Dives
references/operations.md— when runninginfra:preview/infra:up, after a catalog bump to a provider package, or when something reads as deployed but does not work.references/migrations.md— when renaming, re-parenting, or replacing an already-deployed resource, when a review suggests adding analias, or when importing an existing Azure resource withpulumi import --generate-code.references/provider-quirks.md— when picking an Azure Native resource token, naming a Logic App API connection, or touching the GitHubRepositoryresource and branch protection.
Signals
- GitHub stars
- 23
- Forks
- 3
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
pulumi-infra- Source
- github.com/esposter/esposter