{"id":"src_jjcTPC7jQ256vfihHZAgC","slug":"pulumi-blog","name":"Pulumi Blog","type":"feed","url":"https://www.pulumi.com/blog/","orgId":"org_CXx7IPNbZpLaDROi03ZTh","productId":null,"productSlug":null,"org":{"id":"org_CXx7IPNbZpLaDROi03ZTh","slug":"pulumi","name":"Pulumi"},"isPrimary":false,"isHidden":false,"discovery":"curated","metadata":"{\"evaluatedMethod\":\"feed\",\"evaluatedAt\":\"2026-04-11T13:59:04.992Z\",\"feedUrl\":\"https://www.pulumi.com/blog/rss.xml\",\"feedType\":\"rss\",\"feedDiscoveredAt\":\"2026-04-11T13:59:04.992Z\",\"noFeedFound\":false,\"feedEtag\":\"W/\\\"ffa88d7931fc43c01507d2c46a55a891\\\"\",\"feedLastModified\":\"Mon, 20 Jul 2026 21:08:23 GMT\",\"marketingFilter\":true,\"feedContentDepth\":\"summary-only\",\"enrichment\":{\"consecutiveFailures\":3},\"sourceActor\":{\"nextAlarmAt\":\"2026-07-21T01:20:56.058Z\",\"lastAlarmAt\":\"2026-07-20T21:20:56.765Z\",\"managed\":true}}","notice":null,"kind":"platform","stars":null,"starsFetchedAt":null,"releaseCount":92,"releasesLast30Days":16,"avgReleasesPerWeek":3,"latestVersion":null,"latestDate":"2026-07-20T00:00:00.000Z","changelogUrl":null,"hasChangelogFile":false,"lastFetchedAt":"2026-07-20T21:21:29.753Z","lastPolledAt":"2026-07-20T21:21:15.703Z","changeDetectedAt":null,"trackingSince":"2025-12-16T00:00:00.000Z","releases":[{"id":"rel_EYs08lIpOVZT9HDoM5Lqh","version":null,"type":"feature","title":"Incident Response as Code: Managing PagerDuty with Pulumi","summary":"![](https://www.pulumi.com/images/generated/blog/incident-response-as-code-pagerduty-pulumi/index.png)\n\nYou usually find out in the postmortem: the al...","titleGenerated":null,"titleShort":null,"breaking":"unknown","importance":null,"content":"![](https://www.pulumi.com/images/generated/blog/incident-response-as-code-pagerduty-pulumi/index.png)\n\nYou usually find out in the postmortem: the alarm fired, but it paged a schedule nobody was on anymore. Or the service had been running in production for three months before anyone created the matching [PagerDuty](https://www.pagerduty.com/) service, so the first person to notice the outage was a customer. The infrastructure was code, reviewed and versioned. The incident response setup was forty clicks in a web UI, done once, by someone who has since changed teams.\n\nPagerDuty’s own engineering team has been making the case for [managing PagerDuty as code](https://www.pagerduty.com/eng/how-why-terraform/) for years, and the OneUptime folks recently published a [hands-on guide to the PagerDuty Terraform provider](https://oneuptime.com/blog/post/2026-02-23-how-to-configure-pagerduty-provider-in-terraform/view) that walks the whole resource catalog. I liked both posts, and you probably know where this is going: I want the same thing in TypeScript, with the on-call setup and the infrastructure it protects living in the same Pulumi program. Teams, schedules, escalation policies, services, and the CloudWatch alarm that pages you: one `pulumi up`.\n\n## What you are building\n\nThe demo models a small platform team that owns a checkout service on AWS. On the PagerDuty side: a team with three members, a weekly on-call rotation, an escalation policy with two levels, and a service with a CloudWatch integration. On the AWS side: a dead-letter queue, an alarm that fires when a message lands in it, and an SNS topic that forwards the alarm to PagerDuty. The interesting part is where the two meet: the integration key PagerDuty mints flows straight into the SNS subscription without ever touching a clipboard.\n\n```\nflowchart LR\nDLQ[\"SQS dead-letter queue\"] --> Alarm[\"CloudWatch alarm\"]\nAlarm --> SNS[\"SNS topic\"]\nSNS -->|\"HTTPS subscription<br/>with integration key\"| Int[\"CloudWatch integration\"]\nInt --> Svc[\"PagerDuty service<br/>Checkout API\"]\nSvc --> EP[\"Escalation policy\"]\nEP -->|\"level 1\"| Sched[\"Weekly rotation<br/>Lena, Jonas, Mia\"]\nEP -->|\"level 2, after 15 min\"| Lead[\"Team lead\"]\n```\n\nEverything on both sides of that diagram is one TypeScript program. If you would rather write Python, Go, C#, Java, or YAML, the [Pulumi PagerDuty provider](https://www.pulumi.com/registry/packages/pagerduty/) supports all of them. It is bridged from PagerDuty’s official Terraform provider (which started life as a community project by Alexander Hellbom before PagerDuty adopted it), so the resource coverage is identical.\n\n## Prerequisites\n\nBefore getting started, ensure you have:\n\n- [Pulumi CLI](https://www.pulumi.com/docs/iac/download-install/) installed and configured\n- A [Pulumi Cloud account](https://app.pulumi.com/signup)\n- A PagerDuty account with admin access; a [free trial](https://www.pagerduty.com/sign-up/) works fine\n- An [AWS](https://aws.amazon.com/) account for the alarm wiring in the second half\n- Node.js 18+ for the TypeScript program\n\n## The provider and the token\n\nCreate a new project and add the PagerDuty provider:\n\n```bash\n<span class=\"line\"><span class=\"cl\">pulumi new aws-typescript --name pagerduty-oncall\n</span></span><span class=\"line\"><span class=\"cl\">npm install @pulumi/pagerduty\n</span></span>\n```\n\nThe provider authenticates with a REST API key. In PagerDuty, navigate to **Integrations** > **API Access Keys** and create a new key. This is the account-level REST API key, not a user token: user tokens are scoped to what one person can see, and you want the automation to manage the whole account.\n\nI store the key in [Pulumi ESC](https://www.pulumi.com/docs/esc/) so it never lands in a `.env` file, shell history, or agent context:\n\n```bash\n<span class=\"line\"><span class=\"cl\">pulumi env init <your-org>/pagerduty-demo/dev\n</span></span>\n```\n\nAdd the token to the environment definition:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"nt\">values</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">pagerduty</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">token</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">fn::secret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\"><your-rest-api-key></span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">pulumiConfig</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">pagerduty:token</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${pagerduty.token}</span><span class=\"w\">\n</span></span></span>\n```\n\nThen reference the environment from your stack in `Pulumi.dev.yaml`:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"nt\">environment</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"l\"><your-org>/pagerduty-demo/dev</span><span class=\"w\">\n</span></span></span>\n```\n\nIn a hurry? `pulumi config set pagerduty:token --secret` does the same job for a single stack, and the provider also reads the `PAGERDUTY_TOKEN` environment variable. If your account lives on the EU service region, additionally set `pagerduty:serviceRegion` to `eu`.\n\n## Teams, users, and memberships\n\nEverything in PagerDuty hangs off teams and users, so that is where the program starts:\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">pagerduty</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/pagerduty\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">platformTeam</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">Team</span><span class=\"p\">(</span><span class=\"s2\">\"platform-team\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Platform Engineering\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">description</span><span class=\"o\">:</span> <span class=\"s2\">\"Owns the checkout path and the shared platform\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">lena</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">User</span><span class=\"p\">(</span><span class=\"s2\">\"lena\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Lena Fischer\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">email</span><span class=\"o\">:</span> <span class=\"s2\">\"lena@example.com\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">jonas</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">User</span><span class=\"p\">(</span><span class=\"s2\">\"jonas\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Jonas Weber\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">email</span><span class=\"o\">:</span> <span class=\"s2\">\"jonas@example.com\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">mia</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">User</span><span class=\"p\">(</span><span class=\"s2\">\"mia\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Mia Schneider\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">email</span><span class=\"o\">:</span> <span class=\"s2\">\"mia@example.com\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">TeamMembership</span><span class=\"p\">(</span><span class=\"s2\">\"lena-platform\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">userId</span>: <span class=\"kt\">lena.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">teamId</span>: <span class=\"kt\">platformTeam.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">role</span><span class=\"o\">:</span> <span class=\"s2\">\"manager\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">TeamMembership</span><span class=\"p\">(</span><span class=\"s2\">\"jonas-platform\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">userId</span>: <span class=\"kt\">jonas.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">teamId</span>: <span class=\"kt\">platformTeam.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">role</span><span class=\"o\">:</span> <span class=\"s2\">\"responder\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">TeamMembership</span><span class=\"p\">(</span><span class=\"s2\">\"mia-platform\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">userId</span>: <span class=\"kt\">mia.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">teamId</span>: <span class=\"kt\">platformTeam.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">role</span><span class=\"o\">:</span> <span class=\"s2\">\"responder\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\nThree users, one team, three memberships. Reorgs stop being an afternoon of UI archaeology and become a pull request where the diff shows exactly who moved where.\n\nIf your users already come from SSO or SCIM provisioning (they usually do), don’t create them in the program. Look them up instead with the `getUser` data source: `pagerduty.getUserOutput({ email: \"lena@yourcompany.com\" })`. And if your account restricts sign-ups to your company’s email domain, the API enforces that too, so swap the `example.com` addresses for ones on your own domain when trying this out.\n\n## A schedule people can live with\n\nThe on-call schedule is where incident response meets real life, so it deserves more care than a demo usually gives it. If you have seen PagerDuty schedules in Terraform before, you probably remember the `Schedule` resource and its `layers`. That resource is deprecated in the current provider because it speaks the legacy v1 schedules API; its successor `Schedulev2` uses PagerDuty’s new model built on rotations, events, and RFC 5545 recurrence rules:\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">primary</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">Schedulev2</span><span class=\"p\">(</span><span class=\"s2\">\"primary\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Platform Primary On-Call\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">description</span><span class=\"o\">:</span> <span class=\"s2\">\"Weekly hand-off rotation for the platform team\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">timeZone</span><span class=\"o\">:</span> <span class=\"s2\">\"Europe/Berlin\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">teams</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"nx\">platformTeam</span><span class=\"p\">.</span><span class=\"nx\">id</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">rotations</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">events</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Weekly rotation\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">startTime</span><span class=\"o\">:</span> <span class=\"s2\">\"2026-08-03T09:00:00+02:00\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">endTime</span><span class=\"o\">:</span> <span class=\"s2\">\"2026-08-10T09:00:00+02:00\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">effectiveSince</span><span class=\"o\">:</span> <span class=\"s2\">\"2026-08-03T07:00:00Z\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">recurrences</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"RRULE:FREQ=WEEKLY;BYDAY=MO\"</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">assignmentStrategies</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">type</span><span class=\"o\">:</span> <span class=\"s2\">\"rotating_member_assignment_strategy\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">shiftsPerMember</span>: <span class=\"kt\">1</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">members</span><span class=\"o\">:</span> <span class=\"p\">[</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span> <span class=\"kr\">type</span><span class=\"o\">:</span> <span class=\"s2\">\"user_member\"</span><span class=\"p\">,</span> <span class=\"nx\">userId</span>: <span class=\"kt\">lena.id</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span> <span class=\"kr\">type</span><span class=\"o\">:</span> <span class=\"s2\">\"user_member\"</span><span class=\"p\">,</span> <span class=\"nx\">userId</span>: <span class=\"kt\">jonas.id</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span> <span class=\"kr\">type</span><span class=\"o\">:</span> <span class=\"s2\">\"user_member\"</span><span class=\"p\">,</span> <span class=\"nx\">userId</span>: <span class=\"kt\">mia.id</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\nRead it from the inside out. The event is one shift: it starts Monday at 09:00 Berlin time and ends the following Monday at 09:00, and the RRULE makes it recur weekly. The assignment strategy hands each occurrence to the next member in the list, so Lena, Jonas, and Mia each take a full week, round robin. `effectiveSince` tells PagerDuty when this configuration starts producing shifts. Business-hours-only coverage falls out of the same model: shorten the shift window and change the recurrence to `RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR`.\n\n## The escalation policy\n\nThe escalation policy reads top to bottom, the same way the incident will play out:\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">escalation</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">EscalationPolicy</span><span class=\"p\">(</span><span class=\"s2\">\"platform-escalation\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Platform Escalation Policy\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">numLoops</span>: <span class=\"kt\">2</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">teams</span>: <span class=\"kt\">platformTeam.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">rules</span><span class=\"o\">:</span> <span class=\"p\">[</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">escalationDelayInMinutes</span>: <span class=\"kt\">15</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">targets</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">type</span><span class=\"o\">:</span> <span class=\"s2\">\"schedule_reference\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">id</span>: <span class=\"kt\">primary.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">escalationDelayInMinutes</span>: <span class=\"kt\">10</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">targets</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">type</span><span class=\"o\">:</span> <span class=\"s2\">\"user_reference\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">id</span>: <span class=\"kt\">lena.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\nPage whoever the primary schedule says is on call. If nobody acknowledges within 15 minutes, page Lena directly. If the whole chain runs dry, `numLoops: 2` starts it over twice more before PagerDuty gives up. When someone asks what actually happens when this fires at 3 AM, the answer is a dozen lines of reviewed code, not a screenshot of a UI that may or may not be current.\n\n## The service\n\nA PagerDuty service is the unit incidents attach to, and it is where the escalation policy gets wired in:\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">checkoutService</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">Service</span><span class=\"p\">(</span><span class=\"s2\">\"checkout\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Checkout API\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">escalationPolicy</span>: <span class=\"kt\">escalation.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">autoResolveTimeout</span><span class=\"o\">:</span> <span class=\"s2\">\"14400\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">acknowledgementTimeout</span><span class=\"o\">:</span> <span class=\"s2\">\"600\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\nThink of a service as the thing that can break, like the checkout API or the payments queue behind it. Every incident in PagerDuty opens against exactly one service, and the service decides what happens next. The escalation policy answers who gets paged. `acknowledgementTimeout` re-triggers an incident that was acknowledged and then went quiet for ten minutes, so a sleepy “ack” cannot make a problem disappear. And `autoResolveTimeout` closes an incident that has sat open for four hours, which keeps the dashboard honest when everyone forgot to press resolve. Map services one-to-one to the things your team actually owns, and the service directory turns into an honest inventory of your system, each entry with an owner attached.\n\n## The payoff: the alarm and the pager in one program\n\nSo far this is a nicer syntax for what the Terraform guides already do. Here is where it starts earning its keep. Remember the service from the top of this post that ran for three months without a pager? The place that failure usually hides is a dead-letter queue, where bad news piles up quietly. This is what it looks like when the queue, the alarm, and the pager live in one program:\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">aws</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/aws\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">pulumi</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/pulumi\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">cloudwatch</span> <span class=\"o\">=</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">getVendorOutput</span><span class=\"p\">({</span> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Amazon CloudWatch\"</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">checkoutIntegration</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">ServiceIntegration</span><span class=\"p\">(</span><span class=\"s2\">\"checkout-cloudwatch\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Amazon CloudWatch\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">service</span>: <span class=\"kt\">checkoutService.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">vendor</span>: <span class=\"kt\">cloudwatch.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">alarmTopic</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">sns</span><span class=\"p\">.</span><span class=\"nx\">Topic</span><span class=\"p\">(</span><span class=\"s2\">\"checkout-alarms\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">new</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">sns</span><span class=\"p\">.</span><span class=\"nx\">TopicSubscription</span><span class=\"p\">(</span><span class=\"s2\">\"checkout-alarms-to-pagerduty\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">topic</span>: <span class=\"kt\">alarmTopic.arn</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">protocol</span><span class=\"o\">:</span> <span class=\"s2\">\"https\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">endpoint</span>: <span class=\"kt\">pulumi.interpolate</span><span class=\"sb\">`https://events.pagerduty.com/integration/</span><span class=\"si\">${</span><span class=\"nx\">checkoutIntegration</span><span class=\"p\">.</span><span class=\"nx\">integrationKey</span><span class=\"si\">}</span><span class=\"sb\">/enqueue`</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">endpointAutoConfirms</span>: <span class=\"kt\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">dlq</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">sqs</span><span class=\"p\">.</span><span class=\"nx\">Queue</span><span class=\"p\">(</span><span class=\"s2\">\"checkout-dlq\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">new</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">cloudwatch</span><span class=\"p\">.</span><span class=\"nx\">MetricAlarm</span><span class=\"p\">(</span><span class=\"s2\">\"checkout-dlq-alarm\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">alarmDescription</span><span class=\"o\">:</span> <span class=\"s2\">\"Messages are landing in the checkout dead-letter queue\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">namespace</span><span class=\"o\">:</span> <span class=\"s2\">\"AWS/SQS\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">metricName</span><span class=\"o\">:</span> <span class=\"s2\">\"ApproximateNumberOfMessagesVisible\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">dimensions</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">QueueName</span>: <span class=\"kt\">dlq.name</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">statistic</span><span class=\"o\">:</span> <span class=\"s2\">\"Maximum\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">period</span>: <span class=\"kt\">60</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">evaluationPeriods</span>: <span class=\"kt\">1</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">threshold</span>: <span class=\"kt\">1</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">comparisonOperator</span><span class=\"o\">:</span> <span class=\"s2\">\"GreaterThanOrEqualToThreshold\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">alarmActions</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"nx\">alarmTopic</span><span class=\"p\">.</span><span class=\"nx\">arn</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">okActions</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"nx\">alarmTopic</span><span class=\"p\">.</span><span class=\"nx\">arn</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\nFollow the integration key. PagerDuty mints it when the `ServiceIntegration` is created, and a few lines later it is interpolated into the SNS subscription endpoint. In the two-tool world, this is the step where you open the PagerDuty UI, copy the integration URL, and paste it into a variables file or a wiki page, which is exactly the step that gets skipped at 5 PM on a Friday. Here it cannot be skipped, because it is a dependency: Pulumi will not create the subscription before the integration exists.\n\n`endpointAutoConfirms: true` matters because PagerDuty answers the SNS subscription handshake on its own; without it, the subscription sits in “pending confirmation” forever. And since the alarm sends its OK state to the same topic, PagerDuty resolves the incident automatically once the queue drains.\n\nTearing down the service? The alarm, the topic, the subscription, and the PagerDuty service all go in the same `pulumi destroy`, which spares you the orphaned PagerDuty service that keeps paging people about infrastructure that no longer exists. I have seen that one too.\n\n## Azure and Google Cloud, same trick\n\nNothing about this is AWS-specific. It is always the same two steps: PagerDuty mints a key, the monitoring side consumes it. On Google Cloud, Cloud Monitoring has a native PagerDuty channel type, and its `serviceKey` is the integration key of an Events API v2 integration:\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">gcp</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/gcp\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">gcpIntegration</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">ServiceIntegration</span><span class=\"p\">(</span><span class=\"s2\">\"checkout-gcp\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Google Cloud Monitoring\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">service</span>: <span class=\"kt\">checkoutService.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">type</span><span class=\"o\">:</span> <span class=\"s2\">\"events_api_v2_inbound_integration\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">new</span> <span class=\"nx\">gcp</span><span class=\"p\">.</span><span class=\"nx\">monitoring</span><span class=\"p\">.</span><span class=\"nx\">NotificationChannel</span><span class=\"p\">(</span><span class=\"s2\">\"checkout-pagerduty\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">displayName</span><span class=\"o\">:</span> <span class=\"s2\">\"PagerDuty: Checkout API\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">type</span><span class=\"o\">:</span> <span class=\"s2\">\"pagerduty\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">sensitiveLabels</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">serviceKey</span>: <span class=\"kt\">gcpIntegration.integrationKey</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\nOn Azure, an Azure Monitor action group with a webhook receiver does the same job, pointed at the familiar enqueue URL (with `resourceGroup` being an `azure.resources.ResourceGroup` from earlier in your program):\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">azure</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/azure-native\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">azureVendor</span> <span class=\"o\">=</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">getVendorOutput</span><span class=\"p\">({</span> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Microsoft Azure\"</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">azureIntegration</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">ServiceIntegration</span><span class=\"p\">(</span><span class=\"s2\">\"checkout-azure\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Microsoft Azure\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">service</span>: <span class=\"kt\">checkoutService.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">vendor</span>: <span class=\"kt\">azureVendor.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">new</span> <span class=\"nx\">azure</span><span class=\"p\">.</span><span class=\"nx\">monitor</span><span class=\"p\">.</span><span class=\"nx\">ActionGroup</span><span class=\"p\">(</span><span class=\"s2\">\"checkout-pagerduty\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">resourceGroupName</span>: <span class=\"kt\">resourceGroup.name</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">location</span><span class=\"o\">:</span> <span class=\"s2\">\"global\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">groupShortName</span><span class=\"o\">:</span> <span class=\"s2\">\"checkout\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">enabled</span>: <span class=\"kt\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">webhookReceivers</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"pagerduty\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">serviceUri</span>: <span class=\"kt\">pulumi.interpolate</span><span class=\"sb\">`https://events.pagerduty.com/integration/</span><span class=\"si\">${</span><span class=\"nx\">azureIntegration</span><span class=\"p\">.</span><span class=\"nx\">integrationKey</span><span class=\"si\">}</span><span class=\"sb\">/enqueue`</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">useCommonAlertSchema</span>: <span class=\"kt\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\nThe `location: \"global\"` is not a placeholder. Action groups are a global service, and Azure rejects regular regions like `westeurope` for them; setting it explicitly makes that intent obvious in the code.\n\nSame program, same key handoff, different cloud. If your platform spans all three, the PagerDuty side does not care.\n\n## Breaking a Fargate payment service on purpose\n\nEverything so far wires configuration together, but I did not want to publish wiring I had never watched fire. So I put it to the test: a companion project that deploys a small payment service on [AWS Fargate](https://aws.amazon.com/fargate/) together with its entire failure chain, and then breaks it on purpose.\n\nThe worker is deliberately boring: the plain AWS CLI container image running a shell loop. It receives payment messages from an SQS queue, processes anything that has an `amount`, and refuses everything else. A refused message goes back to the queue, and after three failed receives SQS moves it to the dead-letter queue. From there, the chain from earlier in this post takes over: DLQ alarm, SNS topic, CloudWatch integration, escalation policy, phone.\n\nA valid payment flows through in seconds:\n\n```bash\n<span class=\"line\"><span class=\"cl\">./scripts/send-payment.sh\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># worker log: processed payment: {\"orderId\":\"4711\",\"amount\":\"49.99\",\"currency\":\"EUR\"}</span>\n</span></span>\n```\n\nA payment without an amount poisons the queue:\n\n```bash\n<span class=\"line\"><span class=\"cl\">./scripts/inject-failure.sh\n</span></span>\n```\n\nHere is the timeline from my test run, as the test script observed it through the AWS and PagerDuty APIs:\n\nElapsed\n\nWhat happened\n\n0:00\n\nPoison payment injected\n\n3:45\n\nThird receive failed, message redriven to the DLQ, alarm in `ALARM`\n\n3:49\n\nPagerDuty incident triggered on Payment Service\n\n3:50\n\nRecovery: dead-letter queue purged\n\n6:48\n\nAlarm back to `OK`\n\n6:49\n\nIncident resolved\n\n![The PagerDuty incidents view showing the triggered incident: the dead-letter queue alarm on Payment Service, high urgency, assigned to the on-call engineer](pagerduty-incident.png)\n\nUnder four minutes from a bad message to a page, and I never touched the PagerDuty UI in either direction; the incident opened and closed itself off the alarm’s state transitions. The incident timeline spells it out: automatically triggered and assigned, then resolved through the integration API.\n\n![The resolved incident’s timeline in PagerDuty: automatically triggered and assigned at 9:58 AM, the on-call notified by email, and resolved through the integration API 12 minutes later](pagerduty-resolved.png)\n\nThe full program, the worker loop, and the inject and recover scripts are in the companion repo:\n\n[![GitHub repository: dirien/pulumi-pagerduty-fargate-demo](https://opengraph.githubassets.com/1/dirien/pulumi-pagerduty-fargate-demo)\n\ngithub.com/dirien/pulumi-pagerduty-fargate-demo\n\n](https://github.com/dirien/pulumi-pagerduty-fargate-demo)\n\n## Growing without the drift: a reusable component\n\nWhether any of this stays consistent as your organization grows comes down to defaults. If creating a properly paged service takes a checklist, people will skip items. If it takes a [component](https://www.pulumi.com/docs/iac/concepts/components/), they won’t, because the component is less work than the checklist. So wrap the service, the integration, and the alarm topic into one:\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">class</span> <span class=\"nx\">OnCallService</span> <span class=\"kr\">extends</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">ComponentResource</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">public</span> <span class=\"kr\">readonly</span> <span class=\"nx\">service</span>: <span class=\"kt\">pagerduty.Service</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">public</span> <span class=\"kr\">readonly</span> <span class=\"nx\">alarmTopic</span>: <span class=\"kt\">aws.sns.Topic</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">constructor</span><span class=\"p\">(</span><span class=\"nx\">name</span>: <span class=\"kt\">string</span><span class=\"p\">,</span> <span class=\"nx\">args</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">displayName</span>: <span class=\"kt\">string</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">escalationPolicy</span>: <span class=\"kt\">pulumi.Input</span><span class=\"p\"><</span><span class=\"nt\">string</span><span class=\"p\">>;</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span> <span class=\"nx\">opts?</span>: <span class=\"kt\">pulumi.ComponentResourceOptions</span><span class=\"p\">)</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">super</span><span class=\"p\">(</span><span class=\"s2\">\"acme:oncall:OnCallService\"</span><span class=\"p\">,</span> <span class=\"nx\">name</span><span class=\"p\">,</span> <span class=\"p\">{},</span> <span class=\"nx\">opts</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">this</span><span class=\"p\">.</span><span class=\"nx\">service</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">Service</span><span class=\"p\">(</span><span class=\"nx\">name</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span>: <span class=\"kt\">args.displayName</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">escalationPolicy</span>: <span class=\"kt\">args.escalationPolicy</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">autoResolveTimeout</span><span class=\"o\">:</span> <span class=\"s2\">\"14400\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">acknowledgementTimeout</span><span class=\"o\">:</span> <span class=\"s2\">\"600\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span> <span class=\"p\">{</span> <span class=\"nx\">parent</span>: <span class=\"kt\">this</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">const</span> <span class=\"nx\">cloudwatch</span> <span class=\"o\">=</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">getVendorOutput</span><span class=\"p\">({</span> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Amazon CloudWatch\"</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">const</span> <span class=\"nx\">integration</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pagerduty</span><span class=\"p\">.</span><span class=\"nx\">ServiceIntegration</span><span class=\"p\">(</span><span class=\"nx\">name</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Amazon CloudWatch\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">service</span>: <span class=\"kt\">this.service.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">vendor</span>: <span class=\"kt\">cloudwatch.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span> <span class=\"p\">{</span> <span class=\"nx\">parent</span>: <span class=\"kt\">this</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">this</span><span class=\"p\">.</span><span class=\"nx\">alarmTopic</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">sns</span><span class=\"p\">.</span><span class=\"nx\">Topic</span><span class=\"p\">(</span><span class=\"nx\">name</span><span class=\"p\">,</span> <span class=\"p\">{},</span> <span class=\"p\">{</span> <span class=\"nx\">parent</span>: <span class=\"kt\">this</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">new</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">sns</span><span class=\"p\">.</span><span class=\"nx\">TopicSubscription</span><span class=\"p\">(</span><span class=\"nx\">name</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">topic</span>: <span class=\"kt\">this.alarmTopic.arn</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">protocol</span><span class=\"o\">:</span> <span class=\"s2\">\"https\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">endpoint</span>: <span class=\"kt\">pulumi.interpolate</span><span class=\"sb\">`https://events.pagerduty.com/integration/</span><span class=\"si\">${</span><span class=\"nx\">integration</span><span class=\"p\">.</span><span class=\"nx\">integrationKey</span><span class=\"si\">}</span><span class=\"sb\">/enqueue`</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">endpointAutoConfirms</span>: <span class=\"kt\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span> <span class=\"p\">{</span> <span class=\"nx\">parent</span>: <span class=\"kt\">this</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">this</span><span class=\"p\">.</span><span class=\"nx\">registerOutputs</span><span class=\"p\">({});</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span>\n```\n\nAnd now every new service ships with a pager attached. The `checkout` instance here replaces the hand-rolled service, integration, and topic from earlier:\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">checkout</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">OnCallService</span><span class=\"p\">(</span><span class=\"s2\">\"checkout\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">displayName</span><span class=\"o\">:</span> <span class=\"s2\">\"Checkout API\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">escalationPolicy</span>: <span class=\"kt\">escalation.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">payments</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">OnCallService</span><span class=\"p\">(</span><span class=\"s2\">\"payments\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">displayName</span><span class=\"o\">:</span> <span class=\"s2\">\"Payments API\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">escalationPolicy</span>: <span class=\"kt\">escalation.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\nApp teams get one contract: point your alarms at `alarmTopic`. The escalation policy, the timeouts, the integration, the SNS wiring, all of it becomes the paved road instead of a checklist item. This is the same [components-and-templates story](https://www.pulumi.com/blog/golden-paths-infrastructure-components-and-templates/) I keep telling for compute and networking; incident response just turns out to be another thing a platform team can pave.\n\n## Importing what you already clicked together\n\nNobody starts from an empty PagerDuty account. The realistic adoption path is `pulumi import`, resource by resource:\n\n```bash\n<span class=\"line\"><span class=\"cl\">pulumi import pagerduty:index/team:Team platform-team PXXXXXX\n</span></span><span class=\"line\"><span class=\"cl\">pulumi import pagerduty:index/service:Service checkout PYYYYYY\n</span></span>\n```\n\nThe IDs are in the URL when you open a team or service in the PagerDuty web UI, and `pulumi import` writes the matching code for you, so bringing an existing setup under management is mostly a copy-paste loop. The [import guide](https://www.pulumi.com/docs/iac/guides/migration/import/) covers the mechanics.\n\nOne habit matters after that, and PagerDuty’s engineering post makes the same point about Terraform: once a resource is managed as code, stop editing it in the UI. Changes made in the web console are drift, and you will want to run `pulumi refresh` (or let [drift detection](https://www.pulumi.com/blog/day-2-operations-drift-detection-and-remediation/) do it on a schedule) to catch them before the 3 AM page tests your assumptions for you.\n\n## Conclusion\n\nThe pitch for managing PagerDuty as code has less to do with YAML versus HCL versus TypeScript than with the gap between two systems that each hold half the truth. Your infrastructure code knows a queue exists; PagerDuty knows who gets paged when it backs up. Keep them in separate tools and that gap is maintained by hand, which means it is maintained until the person who cared about it leaves.\n\nPutting both sides in one Pulumi program closes the gap structurally. The integration key becomes a dependency edge instead of a wiki entry, and forgetting the escalation policy is not even possible, because `escalationPolicy` is a required property and the program will not compile without it. That is the version of “nothing gets forgotten” I am willing to rely on.\n\nTo go further:\n\n- The [PagerDuty provider in the Pulumi Registry](https://www.pulumi.com/registry/packages/pagerduty/) covers the full resource catalog: event orchestrations, maintenance windows, response plays, and more.\n- [Pulumi ESC](https://www.pulumi.com/docs/esc/) keeps the API token out of your shell history.\n- Coming from the Terraform provider? Your resource knowledge maps one-to-one, and the [migration guide](https://www.pulumi.com/docs/iac/guides/migration/migrating-to-pulumi/from-terraform/) covers the rest.\n\nIf you build something with this, or if your on-call setup has a horror story that code would have prevented, come tell me in the [Pulumi Community Slack](https://slack.pulumi.com/).","publishedAt":"2026-07-20T00:00:00.000Z","fetchedAt":"2026-07-20T17:21:21.720Z","url":"https://www.pulumi.com/blog/incident-response-as-code-pagerduty-pulumi/","media":[{"type":"image","url":"https://www.pulumi.com/images/generated/blog/incident-response-as-code-pagerduty-pulumi/index.png","r2Key":"releases/ccd248f83005f6e0e33797cdef1c94dac5012ee6f7ff9ca25ead89c1119910b3.png","r2Url":"https://media.releases.sh/releases/ccd248f83005f6e0e33797cdef1c94dac5012ee6f7ff9ca25ead89c1119910b3.png"},{"type":"image","url":"https://opengraph.githubassets.com/1/dirien/pulumi-pagerduty-fargate-demo","alt":"GitHub repository: dirien/pulumi-pagerduty-fargate-demo","r2Key":"releases/93e8674807ad31cb88d5f3a7b3c0278e7941894d11f45b7aea248261da8b2a49.png","r2Url":"https://media.releases.sh/releases/93e8674807ad31cb88d5f3a7b3c0278e7941894d11f45b7aea248261da8b2a49.png"}],"coverageCount":0},{"id":"rel_2S0bqFkOiignZ-Ah-hQ4p","version":null,"type":"feature","title":"Best Terraform Alternatives in 2026","summary":"Blog post surveys infrastructure-as-code alternatives to Terraform in 2026, covering general-purpose-language platforms (Pulumi, AWS CDK), HCL-compatible forks (OpenTofu), and cloud-specific tools (CloudFormation, Azure Bicep, Crossplane). Analysis highlights licensing shifts, HCL's composability ceiling, and AI agent compatibility as key factors driving re-evaluation.","titleGenerated":"Pulumi evaluates top Terraform alternatives in 2026","titleShort":"Top Terraform alternatives in 2026 evaluated","breaking":"none","importance":3,"content":"![](https://www.pulumi.com/images/generated/blog/best-terraform-alternatives/index.png)\n\nThe strongest Terraform alternatives in 2026 split into three groups: general-purpose-language platforms like Pulumi and AWS CDK, HCL-compatible forks like OpenTofu, and cloud- or platform-specific tools like AWS CloudFormation, Azure Bicep, and Crossplane. Which one fits depends less on syntax preference than on how well it lets your team, and increasingly your AI coding agents, read, test, and safely change infrastructure code.\n\n## Why teams are re-evaluating Terraform in 2026\n\nTerraform has been the default infrastructure-as-code tool for most of the last decade, and for good reason: a mature provider ecosystem, a large community, and a state model that most platform teams have learned to live with. But three forces are pushing teams to look again at what else is available.\n\nThe first is licensing. HashiCorp moved Terraform from the open-source Mozilla Public License to the Business Source License in August 2023, a shift that triggered the community fork now known as OpenTofu. HashiCorp itself became a wholly owned subsidiary of IBM when that acquisition closed in February 2025. Neither event breaks anything for existing Terraform users, but both changed how governance and long-term product direction get decided, and that’s enough for some platform teams to want a documented Plan B.\n\nThe second is the accumulated cost of working in a domain-specific language. HCL wasn’t designed for the abstraction, composition, and testing patterns that platform engineering now expects: sharing logic across teams, writing meaningful unit tests, and building internal libraries that read like software rather than templated configuration. Terraform has closed some of this gap over time, adding a native test framework in version 1.6, but the ceiling on what a declarative DSL can express is still lower than what a general-purpose language offers.\n\nThe third, and the one reshaping the conversation fastest, is agentic AI. Coding agents like GitHub Copilot, Claude, and Pulumi’s own Neo are now a standing part of how engineering teams write and review code, and infrastructure is not exempt. These agents were trained on enormous volumes of real Python, TypeScript, Go, and Java, plus their entire testing and tooling ecosystems, but on comparatively little well-formed HCL. When an agent can write and modify infrastructure in a language it already understands deeply, the feedback loop of propose, test, preview, and ship gets tighter. When it has to reason about a purpose-built DSL, that loop lengthens and the agent is more likely to produce configuration nobody would write by hand. That gap doesn’t disqualify Terraform, but it does mean “which IaC tool works best with AI agents” is now a first-class question when teams evaluate alternatives, alongside the perennial ones about multi-cloud reach and governance.\n\nNone of this is an argument that Terraform is going away. It remains a capable, widely adopted tool with a genuinely large provider catalog. It’s an argument that 2026 is a reasonable moment to look at what else exists, honestly, and pick the tool that fits where your infrastructure and your engineering workflow are actually headed. For a broader look at how AI agents are changing infrastructure work generally, see [building for agentic infrastructure](https://www.pulumi.com/releases/agentic-infrastructure-era/). For a head-to-head look at Pulumi and Terraform specifically, see our [Pulumi vs. Terraform comparison](https://www.pulumi.com/docs/iac/comparisons/terraform/).\n\n## Pulumi\n\nPulumi is an infrastructure-as-code platform that lets you define cloud infrastructure in general-purpose languages, including Python, TypeScript, Go, C#, and Java, plus a YAML option for teams that prefer a declarative format. Rather than compiling down to another tool’s templates, Pulumi programs run directly against a deployment engine that supports more than 180 providers in total, covering AWS, Azure, Google Cloud, Kubernetes, and a long tail of SaaS and on-prem targets.\n\nWriting infrastructure in a real language means you get the tooling that comes with it for free: IDE autocomplete and type checking, unit and integration test frameworks, package managers for sharing reusable components, and the same code review and CI/CD conventions your application teams already use. It also means Pulumi programs run directly through Pulumi’s own deployment engine against any cloud or SaaS platform, with no separate compile-to-template step in between, unlike CDK-style tools that transpile into another system’s format before anything deploys. For an AI agent iterating on infrastructure, fewer steps between “write code” and “see the result” means a tighter feedback loop. [Pulumi Neo](https://www.pulumi.com/product/neo/), the platform’s infrastructure agent, builds on this directly: it can propose Terraform-to-Pulumi migrations, run policy-checked previews, respond to failures, and open pull requests inside a team’s existing review workflow.\n\nFor platform teams, Pulumi adds built-in guardrails that Terraform typically requires bolting on through third-party tooling: [policy as code](https://www.pulumi.com/what-is/what-is-policy-as-code/) for enforcing organizational rules before a deployment ships, a secrets and configuration management layer (ESC), human-in-the-loop approval gates, and reusable components that teams can publish and consume like any other internal library, in whatever language they use. The tradeoff is ecosystem maturity in the opposite direction from Terraform: Pulumi’s provider catalog, while broad, has a shorter history than Terraform’s for a handful of niche or long-tail providers, and some teams will need to weigh that against the language and workflow benefits.\n\nAt scale, Pulumi customers report concrete outcomes. BMW’s Software Factory manages more than 20,000 cloud resources with Python-based infrastructure code for over 11,000 developers. Wiz manages more than a million cloud resources through Pulumi’s Automation API and Kubernetes operator, pushing hundreds of thousands of daily infrastructure updates across hundreds of data centers. Supabase scaled from a single region to 16 regions and roughly 80,000 resources. Atlassian’s Bitbucket team reported a 50% reduction in infrastructure maintenance time after adopting Pulumi.\n\n## OpenTofu\n\n[OpenTofu](https://www.pulumi.com/docs/iac/comparisons/terraform/opentofu/) is an open-source fork of Terraform, governed by the Linux Foundation rather than a single vendor. It emerged directly from HashiCorp’s August 2023 license change: a group of Terraform users and vendors forked the last MPL-licensed Terraform release and committed to keeping the fork under a permissive open-source license going forward.\n\nThe practical pitch is continuity: OpenTofu aims to stay a close drop-in replacement for Terraform, using the same HCL syntax, the same provider ecosystem (most Terraform providers work unmodified), and largely the same workflow, so teams can migrate with minimal rewriting. Since forking, OpenTofu’s maintainers have also started adding features HashiCorp hadn’t shipped, including state encryption at rest, giving it a case for teams who want to stay in HCL but prioritize community governance over vendor governance.\n\nThe tradeoff is that OpenTofu inherits HCL’s ceiling along with its familiarity. It solves the licensing and governance concern cleanly, but it doesn’t address the testing, composability, or general-purpose-language advantages that come with moving to a platform like Pulumi or AWS CDK, and AI coding agents face the same reasoning gap with OpenTofu’s HCL that they do with Terraform’s.\n\n## AWS CloudFormation\n\nCloudFormation is AWS’s native infrastructure-as-code service, using YAML or JSON templates that AWS itself parses, validates, and rolls back on failure. Because it’s built and operated by AWS, it gets first access to new AWS service features, and its state is managed entirely by AWS rather than a separate backend your team has to configure and secure.\n\nThe obvious limit is scope: CloudFormation only provisions AWS resources, so any team running infrastructure across more than one cloud, or alongside Kubernetes and SaaS resources, will need a second tool for everything outside AWS. Authoring in raw YAML or JSON also means the same testing and abstraction limitations that HCL-based tools face, without even Terraform’s ecosystem of community modules to offset it. Many AWS-focused teams increasingly write CloudFormation stacks through AWS CDK rather than by hand.\n\n## AWS CDK\n\nAWS CDK (Cloud Development Kit) lets you define AWS infrastructure using general-purpose languages including TypeScript, JavaScript, Python, Java, C#, and Go. Under the hood, CDK code compiles down to standard CloudFormation templates, which AWS then deploys the same way it deploys any other CloudFormation stack.\n\nCDK gives AWS-only teams most of the general-purpose-language benefits that Pulumi offers: real loops, functions, tests, and packages, plus the IDE support that comes with a mainstream language. The compile step is the notable difference from a platform like Pulumi, which runs your program directly against cloud provider APIs: CDK synthesizes a CloudFormation template first, then hands that off, which adds a layer between what your code says and what gets deployed, and can slow the feedback loop an AI coding agent depends on when it’s proposing and testing changes iteratively. CDK is also AWS-only, so it doesn’t help teams who need one consistent authoring model across multiple clouds.\n\nIt’s worth distinguishing AWS CDK clearly from HashiCorp’s CDK for Terraform (CDKTF), which offered a similar general-purpose-language wrapper around Terraform’s HCL engine. HashiCorp sunset and archived CDKTF in December 2025, stating in its own FAQ that the project “did not find product-market fit at scale” and redirecting investment to Terraform core instead. Existing CDKTF code still runs, but the project receives no further commits, fixes, or provider updates, which makes it a poor foundation for new work regardless of language preference.\n\n## Crossplane\n\nCrossplane is a Kubernetes-native framework for infrastructure as code, letting platform teams define and provision cloud resources as Kubernetes custom resources, managed by controllers running inside a cluster. Rather than running a separate CLI-driven apply cycle, Crossplane treats the cluster’s control plane as the single source of truth for both application and infrastructure state.\n\nThis model is a natural fit for teams already standardized on Kubernetes as their platform layer: infrastructure gets the same GitOps, RBAC, and reconciliation patterns as application workloads, and platform teams can build self-service abstractions (Crossplane calls these Compositions) that let application developers request infrastructure without learning Crossplane’s own resource model directly. The tradeoff is that Crossplane assumes a Kubernetes-centric operating model; teams without an existing cluster-based platform will be adopting Kubernetes as a prerequisite, not just an infrastructure tool, and Crossplane’s ecosystem of documented, named enterprise deployments is thinner than Terraform’s or Pulumi’s, making it harder to evaluate at-scale track record from public case studies alone.\n\n## Azure Bicep\n\nBicep is Microsoft’s domain-specific language for deploying Azure resources, designed as a cleaner authoring layer over Azure Resource Manager templates. Bicep code transpiles to ARM JSON, but with syntax that’s considerably easier to read and write than hand-authored ARM. It’s open source under the MIT license, and Microsoft’s own documentation recommends it over raw ARM JSON for new Azure-only infrastructure work.\n\nLike CloudFormation, Bicep’s scope is a single cloud, in this case Azure exclusively, so it isn’t a candidate for teams managing infrastructure across providers. It’s also a DSL rather than a general-purpose language, which means the same testing and reuse ceiling as HCL, though for teams fully committed to Azure and nothing else, it remains a well-supported, low-friction choice.\n\n## Ansible\n\nAnsible is an agentless configuration management and automation tool that uses YAML playbooks to describe the desired state of servers, software, and application deployments. Red Hat, which acquired Ansible in 2015 and was itself acquired by IBM in 2019, maintains it today as part of Red Hat Ansible Automation Platform.\n\nAnsible is often grouped with Terraform in “IaC tool” comparisons, but the two solve different problems and are frequently used together rather than as substitutes: Terraform or Pulumi provisions the resource, and Ansible then configures the operating system, installs software, and manages ongoing configuration state on top of it. Ansible does ship cloud provisioning modules and can create resources directly, but its design center is procedural configuration management, not declarative resource provisioning at the scale Terraform-class tools target. Teams evaluating Terraform alternatives for cloud provisioning specifically will usually want a tool from this list’s other categories, with Ansible layered in afterward for day-two configuration.\n\n## Terragrunt\n\nTerragrunt is a thin orchestration wrapper maintained by Gruntwork that sits on top of Terraform or OpenTofu, rather than replacing either one. Its job is keeping multi-environment, multi-module HCL configurations DRY, and coordinating the order in which modules apply across a larger infrastructure codebase.\n\nIt’s worth being precise about what Terragrunt is not: it doesn’t introduce a new language, provider model, or state backend, and it doesn’t address HCL’s testing or abstraction limitations on its own. Teams considering Terragrunt aren’t choosing a Terraform alternative; they’re choosing a companion tool that makes Terraform or OpenTofu easier to operate at scale. If the underlying frustration is with HCL itself rather than with configuration sprawl, Terragrunt won’t resolve it.\n\n## Comparison table\n\nTool\n\nLanguage\n\nCloud coverage\n\nAI-agent readiness\n\nGovernance & policy\n\nBest for\n\nPulumi\n\nPython, TypeScript, Go, C#, Java, YAML\n\n180+ providers, any cloud\n\nHigh — real languages agents are trained on; Neo agent built in\n\nPolicy as code, ESC secrets, human-in-the-loop approvals\n\nTeams standardizing on AI-native, multi-cloud engineering workflows\n\nOpenTofu\n\nHCL\n\nSame provider ecosystem as Terraform\n\nSame as Terraform — DSL limits agent reasoning\n\nCommunity-governed; adding features like state encryption\n\nTerraform users prioritizing open governance with minimal migration\n\nAWS CloudFormation\n\nYAML/JSON\n\nAWS only\n\nLow — templated config, no native testing\n\nAWS-managed state and rollback\n\nTeams fully committed to AWS wanting a fully managed native service\n\nAWS CDK\n\nTypeScript, Python, Java, C#, Go\n\nAWS only (compiles to CloudFormation)\n\nMedium — real languages, but a compile step slows feedback\n\nInherits CloudFormation governance\n\nAWS-only teams wanting general-purpose languages\n\nCrossplane\n\nKubernetes YAML/CRDs\n\nAny cloud, via Kubernetes control plane\n\nMedium — benefits from Kubernetes-native agent tooling\n\nRBAC and GitOps via Kubernetes\n\nPlatform teams already standardized on Kubernetes\n\nAzure Bicep\n\nBicep DSL (compiles to ARM)\n\nAzure only\n\nLow — DSL, no general-purpose tooling\n\nInherits Azure Resource Manager governance\n\nAzure-only teams wanting a cleaner ARM authoring layer\n\nAnsible\n\nYAML playbooks\n\nAny cloud, config-management focused\n\nLow for provisioning; not its design center\n\nRole-based playbooks, limited policy tooling\n\nDay-two configuration management alongside an IaC tool\n\nTerragrunt\n\nHCL (wraps Terraform/OpenTofu)\n\nSame as underlying Terraform/OpenTofu\n\nSame as underlying Terraform/OpenTofu\n\nSame as underlying Terraform/OpenTofu\n\nKeeping large Terraform/OpenTofu codebases DRY, not a language alternative\n\n## How to choose\n\nIf your team is standardizing on a single cloud and wants the deepest, most tightly integrated tooling for it, the native option, CloudFormation for AWS or Bicep for Azure, is a reasonable default, especially for smaller platform teams that don’t need cross-cloud abstraction. If you’re on AWS specifically and want general-purpose languages without leaving the CloudFormation ecosystem, AWS CDK is the more capable choice, with the caveat that its compile-then-deploy step adds a layer between code and infrastructure.\n\nIf your team is already running Kubernetes as its platform layer and wants infrastructure provisioning to follow the same GitOps and RBAC model as application workloads, Crossplane is worth a serious look, understanding that it comes with Kubernetes as a hard dependency.\n\nIf you’re deep in Terraform today, comfortable with HCL, and your primary concern is licensing and governance rather than language or workflow, OpenTofu is the lowest-friction path: same syntax, same providers, community governance instead of vendor governance.\n\nIf your team is investing in AI coding agents as part of its engineering workflow, wants multi-cloud reach without maintaining separate tools per provider, and values real testing, packaging, and code review practices for infrastructure the same way it does for application code, Pulumi is built specifically for that combination. It’s also the most direct migration path for teams currently on Terraform or CDKTF who want to move to a general-purpose language without changing which clouds they manage.\n\n## Frequently asked questions\n\n### Is Terraform still free to use?\n\nYes, for most teams. Terraform’s core CLI and the vast majority of its providers remain free under the Business Source License that HashiCorp adopted in August 2023. The license restricts a narrow set of commercial use cases, mainly building a competing managed offering on top of Terraform, which doesn’t affect ordinary infrastructure teams using it internally.\n\n### What is the best open-source Terraform alternative?\n\nOpenTofu is the closest open-source alternative if you want to keep using HCL and the Terraform provider ecosystem, since it’s a direct fork governed by the Linux Foundation. If open-source licensing matters but you’re open to a different language, Pulumi’s SDKs and CLI are open source (Apache 2.0), and Crossplane is a CNCF project for teams standardized on Kubernetes.\n\n### Which IaC tool works best with AI coding agents?\n\nTools built on general-purpose languages give AI coding agents the strongest foundation, since those agents have far more training exposure to real Python, TypeScript, Go, C#, and Java than to any infrastructure-specific DSL. Pulumi and AWS CDK both fit this category; AWS CDK’s compile step to CloudFormation adds a layer of indirection that Pulumi, which runs directly against provider APIs, doesn’t have.\n\n### Can I migrate from Terraform without rewriting everything?\n\nIt depends on which alternative you choose. Moving to OpenTofu requires essentially no rewriting, since it’s HCL-compatible by design. Moving to Pulumi, AWS CDK, or another general-purpose-language platform does require translating configuration into program code, though tools like Pulumi’s import and conversion tooling, and increasingly AI coding agents themselves, can automate a meaningful share of that translation rather than requiring a manual line-by-line rewrite.\n\n### Is Pulumi a drop-in Terraform replacement?\n\nNot syntactically. Pulumi uses general-purpose programming languages rather than HCL, so moving from Terraform to Pulumi means translating configuration into code rather than reusing files unchanged. What carries over is the underlying model: state, providers, and resources map conceptually in similar ways, and Pulumi provides tooling to import existing Terraform-managed infrastructure and convert Terraform configuration into a starting Pulumi program, which teams typically use as a first draft rather than a finished migration.\n\n### Does OpenTofu support everything Terraform does?\n\nOpenTofu tracks Terraform’s last open-source release closely and remains compatible with most existing Terraform configurations and providers. Since forking, its Linux Foundation-governed maintainers have also shipped features Terraform hadn’t offered, such as state encryption, while newer HashiCorp-only Terraform features naturally won’t appear in OpenTofu unless the community implements equivalents independently.\n\nFor a broader roundup covering the full infrastructure-as-code category rather than Terraform alternatives specifically, see our guide to [the best IaC tools](https://www.pulumi.com/what-is/top-iac-tools/).\n\n## Conclusion\n\nTerraform remains a capable, widely used tool, and for teams with no appetite to change, OpenTofu offers a nearly friction-free path to the same workflow under different governance. But the more interesting question for 2026 isn’t whether Terraform still works, it’s whether your infrastructure tooling can keep pace with how your engineering organization is actually building software now: with AI coding agents as active participants, not just autocomplete. That question favors platforms built on real, general-purpose languages, tested and reviewed the same way application code is, over any tool, new or established, still built around a purpose-specific configuration syntax. Evaluate honestly against your own cloud footprint, existing platform investment, and how central AI-assisted development already is to your team, and the right alternative, or the right reason to stay put, becomes clear quickly.","publishedAt":"2026-07-18T00:00:00.000Z","fetchedAt":"2026-07-20T21:21:24.881Z","url":"https://www.pulumi.com/blog/best-terraform-alternatives/","media":[{"type":"image","url":"https://www.pulumi.com/images/generated/blog/best-terraform-alternatives/index.png","r2Key":"releases/8ebb3a423f1b0188d15ac15909f530de8b75a501e6e834b4a126735baf2a7413.png","r2Url":"https://media.releases.sh/releases/8ebb3a423f1b0188d15ac15909f530de8b75a501e6e834b4a126735baf2a7413.png"}],"coverageCount":0},{"id":"rel_mAuHGaA3-NACURZ0PdIKs","version":null,"type":"feature","title":"Connect Your Cloud Accounts to Pulumi in Minutes","summary":"New Connect cloud accounts wizard discovers every AWS account, Azure subscription, or Google Cloud project in an organization and onboards them all in a single guided flow, handling OIDC setup, ESC environments, and default scans and policies automatically.","titleGenerated":"Pulumi launches Connect cloud accounts wizard to onboard accounts in minutes","titleShort":"Connect cloud accounts wizard ships: bulk discover and onboard in minutes","breaking":"none","importance":4,"content":"![](https://www.pulumi.com/images/generated/blog/connect-your-cloud-accounts-to-pulumi-in-minutes/index.png)\n\n[Pulumi Insights](https://www.pulumi.com/docs/insights/) gives you visibility and governance across your entire cloud footprint, but that visibility is only as complete as the set of accounts you’ve connected. Until now, connecting an account meant repeating a manual setup for each one: OIDC configuration, hand-written [Pulumi ESC](https://www.pulumi.com/docs/esc/) environments, and per-account scan and policy setup. For an organization with dozens or hundreds of AWS accounts, Azure subscriptions, or Google Cloud projects, that per-account friction was the biggest obstacle to getting a complete picture. Today, the new **Connect cloud accounts** wizard removes it: discover every account in your cloud organization, select the ones you want, and onboard them all in a single guided flow.\n\n## From hours of setup to a single flow\n\nThe time savings are real: connecting a batch of accounts across AWS, Azure, and Google Cloud takes about three minutes end to end. Setting up those same accounts with the manual per-account workflow would take hours to days.\n\nThe wizard handles the entire onboarding lifecycle for AWS, Microsoft Azure, and Google Cloud:\n\n- **Bulk discovery**: Authenticate once with your cloud organization and the wizard lists every account, subscription, or project you can access. Accounts that are already connected are recognized and skipped.\n- **Automatic OIDC setup**: The recommended flows configure each account with short-lived credentials based on OpenID Connect (OIDC) and workload identity federation. No long-lived cloud secrets are stored in Pulumi Cloud.\n- **ESC environments, created for you**: The wizard generates [Pulumi ESC](https://www.pulumi.com/docs/esc/) environments that follow the best practices from the manual OIDC guides — environments that previously had to be authored one by one.\n- **Scans and policies from day one**: By default, scheduled discovery scans and a [pre-built policy pack](https://www.pulumi.com/docs/insights/policy/policy-packs/pre-built-packs/) are applied to every account as part of setup: the Pulumi Best Practices pack on the Team and Enterprise editions, or a compliance pack (CIS, or NIST 800-53 for AWS and Google Cloud) on Business Critical.\n\n## How it works\n\nThe wizard walks you through five steps: choose a provider, authenticate, select accounts, configure discovery and policy, and review the results. You can open it from **Insights** > **Accounts** in the Pulumi Cloud console, or from the **Get to know Pulumi** card on the home dashboard.\n\nAuthentication uses each provider’s native federation mechanism: IAM Identity Center (SSO) for AWS, Microsoft Entra ID workload identity federation for Azure, and Workload Identity Federation for Google Cloud. After you sign in, the wizard discovers the accounts in your organization and pre-selects everything that isn’t already connected. You can search, select all, or toggle individual accounts.\n\n![The account picker showing discovered AWS accounts with checkboxes, a search box, and a select all option](account-picker.png)\n\n### Choose the right access level for your security posture\n\nNot every team wants to grant write access on day one. The wizard offers two access levels, and you can set them per account:\n\n- **Build & Manage (read and write)**: Enables the full platform: [Pulumi Neo](https://www.pulumi.com/product/neo/), infrastructure as code, deployments, and policies that remediate issues automatically (Business Critical).\n- **Discovery & Policy (read-only)**: Limited to discovery scanning and inventory. Pulumi can’t modify your infrastructure.\n\nIf your security review requires it, start everything read-only and raise access for specific accounts later.\n\n![The accounts step showing the selected accounts summary, the Build and Manage access level card, and per account access controls](access-level.png)\n\n### Everything set up, nothing hidden\n\nWhen setup completes, the summary shows exactly what was created: the ESC environments grouped by access level, the state of discovery scanning, and the policy pack applied. If any account fails to connect, the summary lists it with the specific error so you can fix the cause and re-run the wizard. Accounts that connected successfully are skipped on retry.\n\n![The summary step showing setup complete with discovery running, the policy pack applied, and the list of created ESC environments](summary.png)\n\nFor security reviewers, the docs include a full accounting of [what the wizard creates](https://www.pulumi.com/docs/insights/discovery/connect-cloud-accounts/#what-the-wizard-creates) in your cloud provider and in Pulumi Cloud: the IAM roles, app registrations, and service accounts on the cloud side, and the ESC environments and Insights accounts on the Pulumi side.\n\n## Get started\n\nThe Connect cloud accounts wizard is available now for all Pulumi Cloud organizations. To connect your first accounts:\n\n1. Navigate to [**Insights** > **Accounts**](https://app.pulumi.com/) in the Pulumi Cloud console and select **Connect cloud accounts**.\n2. Follow the guided flow for AWS, Azure, or Google Cloud.\n3. Explore your [discovered resources](https://www.pulumi.com/docs/insights/discovery/search/) and [policy findings](https://www.pulumi.com/docs/insights/policy/policy-findings/).\n\nTo learn more:\n\n- [Connect cloud accounts documentation](https://www.pulumi.com/docs/insights/discovery/connect-cloud-accounts/) — prerequisites, each wizard step in detail, and troubleshooting\n- [Insights & Governance overview](https://www.pulumi.com/docs/insights/) — full documentation for discovery and policy capabilities\n- [Pulumi ESC](https://www.pulumi.com/docs/esc/) — how the generated environments manage cloud credentials with OIDC","publishedAt":"2026-07-15T00:00:00.000Z","fetchedAt":"2026-07-15T05:19:32.174Z","url":"https://www.pulumi.com/blog/connect-your-cloud-accounts-to-pulumi-in-minutes/","media":[{"type":"image","url":"https://www.pulumi.com/images/generated/blog/connect-your-cloud-accounts-to-pulumi-in-minutes/index.png","r2Key":"releases/ef2cded1d9db1de237c51610678644af4b69327140e1e49204d8588d60f701df.png","r2Url":"https://media.releases.sh/releases/ef2cded1d9db1de237c51610678644af4b69327140e1e49204d8588d60f701df.png"}],"coverageCount":0},{"id":"rel_BerdqhK8cCghwoWmApRFu","version":null,"type":"feature","title":"Introducing Usage Limits for Pulumi Neo","summary":"Usage limits for Pulumi Neo let organizations set a monthly dollar cap that pauses the AI agent when reached. Per-member limits and email notifications at 50%, 80%, 95%, and 100% are also available for paid plan organizations.","titleGenerated":"Pulumi Neo adds monthly usage limits and per-member controls","titleShort":"Neo usage limits: monthly dollar cap and per-member controls","breaking":"none","importance":3,"content":"![](https://www.pulumi.com/images/generated/blog/neo-usage-limits/index.png)\n\n[Pulumi Neo](https://www.pulumi.com/docs/ai/) is an AI agent that takes on real infrastructure work, and it’s natural to want to hand it more and more. Usage limits give you control so you can do exactly that: set a monthly dollar limit, and Neo pauses when your organization reaches it.\n\n## How usage limits work\n\nYour organization limit is a single monthly dollar amount covering all Neo usage across the org. To set one:\n\n1. In the Pulumi Cloud console, navigate to **Settings → Billing & usage → Neo token usage**.\n2. In the **Manage token usage** panel, enter an organization limit.\n3. Save your changes.\n\nWhen usage reaches the limit, Neo pauses for the rest of the billing period and resumes automatically at the start of the next one. An Admin or Billing Manager can raise the limit to resume before then.\n\n![The Manage token usage panel, where an admin sets the organization’s monthly Neo limit and turns on email notifications.](manage-token-usage.png)\n\nEnforcement happens at a natural boundary in Neo’s work, so a task already in progress finishes its current step before pausing. As a result, usage can go a few dollars over the set limit.\n\n## Per-member limits and alerts\n\nYou can also set a separate limit for each member. A member is paused at whichever limit is smaller: their own or the organization’s. For example, a member with a $200 limit under a $150 organization limit pauses at $150, because the organization limit is smaller.\n\n![The per-member limits table, showing each member’s amount used and effective limit for the billing period.](per-member-limits.png)\n\nTurn on **Enable email notifications** to get a heads-up before you reach the limit. Billing admins are alerted at 50%, 80%, and 95% of the organization limit, with a final notice at 100% when Neo pauses.\n\n## Get started\n\nSet your usage limits and stay in control as your organization hands Neo more and more work. Usage limits are available today for organizations on a paid plan, and an **Admin** or **Billing Manager** can set them.\n\n- [Sign in to Pulumi Cloud](https://app.pulumi.com/signin) and set your first organization limit\n- [Read the Neo usage limits documentation](https://www.pulumi.com/docs/ai/usage-limits/) for per-member limits, alerts, and enforcement details\n- [Join the Community Slack](https://slack.pulumi.com/) to share your feedback","publishedAt":"2026-07-14T15:00:00.000Z","fetchedAt":"2026-07-15T01:19:34.927Z","url":"https://www.pulumi.com/blog/neo-usage-limits/","media":[{"type":"image","url":"https://www.pulumi.com/images/generated/blog/neo-usage-limits/index.png","r2Key":"releases/7ccbf967642cb69e495a361e09f5e7ae1a9a738112fa10be8bc824db220741ea.png","r2Url":"https://media.releases.sh/releases/7ccbf967642cb69e495a361e09f5e7ae1a9a738112fa10be8bc824db220741ea.png"}],"coverageCount":0},{"id":"rel_ZXUkhuZVB67Gxq8hRaiyQ","version":null,"type":"feature","title":"Knowledge as Code: The Memory File Just Got a Spec","summary":"Google's Open Knowledge Format (OKF) v0.1 formalizes the Karpathy wiki pattern as a specification: markdown files with YAML frontmatter, organized in a directory tree with reserved index.md and log.md filenames. Platform teams can now store runbooks, decision records, and architecture knowledge as code alongside their Pulumi infrastructure, linked via the resource field mapping to Pulumi URNs.","titleGenerated":"Pulumi explores Open Knowledge Format for agent-accessible infrastructure knowledge","titleShort":"Open Knowledge Format brings agent-readable wiki to infrastructure","breaking":"none","importance":2,"content":"![](https://www.pulumi.com/images/generated/blog/knowledge-as-code-the-memory-file-just-got-a-spec/index.png)\n\nFive weeks ago I wrote that the least glamorous piece of an agent loop is also the one that decides whether it compounds: [memory](https://www.pulumi.com/blog/stop-prompting-design-the-loop/). A markdown file outside the context window that holds what is done, what is next, and what was learned, because the model forgets all of it between runs. Write the memory file before the loop.\n\nWhat I left open, because there was nothing to point at, was the format. My memory file looked nothing like yours, and neither of our agents could read the other’s.\n\nThree days after that post went live, Google shipped an answer.\n\n## The pattern everyone copied\n\n[Andrej Karpathy published a gist in April](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) he called the LLM wiki, and it collected thousands of stars and forks. It’s meant to be pasted straight into a coding agent. Instead of indexing your documents for RAG and re-deriving answers from raw text on every query, the agent builds a wiki and keeps it current: interlinked markdown pages, an `index.md` with a one-line summary per page, a `log.md` recording every change, entity pages that grow as sources come in. Drop in a meeting transcript and the agent reads it, updates a dozen existing pages, fixes the cross-references, and appends to the log in one pass.\n\nIt took off for the same reason wikis usually die. A knowledge base is valuable in exact proportion to the bookkeeping nobody wants to do: summarizing, linking, reconciling contradictions, pruning stale claims. Karpathy’s line, which Google now quotes back in its own announcement, is that LLMs “don’t get bored, don’t forget to update a cross-reference, and can touch 15 files in one pass.” The human curates sources and asks questions. The agent does the janitorial work that made every previous wiki rot.\n\n## A wiki only your agent can read\n\nThen everyone built one, and every one is a dialect. Mine links related pages in the frontmatter; yours links them at the bottom of the body. Mine has a `tags` field; yours calls it `categories`. None of this matters while the wiki serves one person, because the schema lives in your `CLAUDE.md` and your agent reads it on every run.\n\nIt matters the moment the wiki has to travel. Hand your knowledge base to a teammate and their agent starts guessing at your conventions, or misses half the structure entirely. A platform team that wants one shared wiki, queried independently by everyone’s agents, has no format to agree on. A knowledge base you can’t hand to someone else’s agent is a silo of one. And a team’s collective knowledge should outlive any single person’s markdown habits.\n\n## What Google actually shipped: the Open Knowledge Format\n\nOn June 12, two tech leads in Google Cloud’s data analytics engineering org [announced the Open Knowledge Format](https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing/), with a [spec and reference tooling on GitHub](https://github.com/GoogleCloudPlatform/knowledge-catalog). Strip the branding and OKF v0.1 is a formalization of the Karpathy pattern:\n\n- A bundle is a directory tree of markdown files. Two filenames are reserved: `index.md` for progressive disclosure and `log.md` for update history. Everything else is a concept document.\n- Every concept carries YAML frontmatter with exactly one required field: `type`. Five more are recommended: `title`, `description`, `resource`, `tags`, `timestamp`. Producers can add anything; consumers must preserve what they don’t understand.\n- A markdown link from one concept to another asserts a relationship. The prose around the link says what kind.\n\nThe whole spec fits on a page, and that’s deliberate. The announcement names three principles: minimally opinionated; producers and consumers independently swappable; a format rather than a platform. No required SDK, no compression scheme, no new runtime. Reading a bundle is `cat`; distributing one is `git clone`. The repo backs it up with reference tooling: an enrichment agent that drafts concept docs from BigQuery datasets, and a static visualizer that renders a bundle as a graph. Three sample bundles, built by that same agent, give you something to copy from.\n\n## The critique is the feature\n\nThe pushback writes itself: there is almost nothing here. Folder indexes and five suggested frontmatter fields, on top of a pattern the community already had. True. Also the point.\n\nThe standards that stick are embarrassingly small. [MCP](https://www.pulumi.com/docs/ai/mcp-server/) didn’t model your tools; it standardized the socket they plug into. `AGENTS.md` fixed nothing but a filename, and that was enough for [Neo to read the same conventions file as every other agent](https://www.pulumi.com/blog/pulumi-neo-now-supports-agentsmd/). [Agent Skills](https://www.pulumi.com/docs/ai/skills/) gave procedure a place to live, and docs sites settled on [/llms.txt](https://www.pulumi.com/llms.txt) for published content. Knowledge was the layer still missing its boring little standard. Whether OKF specifically is the one that survives matters less than the shape it commits to: markdown, frontmatter, git. Every other layer of the agent stack already landed there, so that bet is safe.\n\n## Knowledge as code\n\nPlatform teams should care about this before anyone else. Infrastructure as code captures the what: the resources, the config, the dependency graph. It has never captured the why. The runbook for rotating credentials, the decision record explaining the pinned CNI version, who owns the cluster, what the incident in November taught you. That knowledge lives in a wiki behind an API, a shared drive, a Slack thread, and the heads of two senior engineers. Google’s announcement frames this fragmentation as the problem: agents reassembling answers from systems that each speak a proprietary format.\n\nOKF’s answer is a move platform engineers will recognize, because it’s the same one that produced IaC: put it in git, make it diffable, review changes in pull requests. Knowledge as code, sitting next to the infrastructure as code it explains:\n\n```\nplatform/\n├── index.md\n├── log.md\n├── services/\n│ ├── index.md\n│ ├── checkout-api.md\n│ └── payments-worker.md\n├── runbooks/\n│ ├── index.md\n│ └── rotate-database-credentials.md\n└── decisions/\n├── index.md\n└── why-we-pin-the-vpc-cni-version.md\n```\n\nThe recommended `resource` field takes a URI naming the asset a concept describes, and a Pulumi URN is exactly that:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"nn\">---</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">runbook</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">title</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">Rotate the payments database credentials</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">description</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">Zero-downtime credential rotation for the payments Postgres instance.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">resource</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">urn:pulumi:prod::payments::aws:rds/instance:Instance::payments-db</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">tags</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"p\">[</span><span class=\"l\">payments, postgres]</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">timestamp</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"ld\">2026-07-01T09:30:00Z</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nn\">---</span><span class=\"w\">\n</span></span></span>\n```\n\nNow the runbook names the exact resource it operates on. An agent planning a change to that database can walk from the URN to the runbook to the decision record that explains the constraint, before it proposes anything. In the loop post I said [skills](https://www.pulumi.com/docs/ai/skills/) are intent written down, the conventions an agent reads instead of guessing. A knowledge bundle is the other half: experience written down. Tools give the loop hands, skills give it habits, and the bundle is what it gets to remember.\n\nBefore writing any of that down, I ran the test. A fresh agent got two inputs, the spec and this bundle, and one question: which services touch the payments database, and how do I rotate its credentials without downtime? It read six files and answered correctly, down to why rotating a second time too soon is the dangerous move. What sold me was the file it didn’t read. It left the CNI decision record closed, because the cross-links mark that page as a cluster concern, not a database one.\n\nThe bundle is public, exactly the tree above. Clone it, hand your agent the spec, and ask your own questions:\n\n[![GitHub repository: dirien/pulumi-platform-okf-bundle](https://opengraph.githubassets.com/1/dirien/pulumi-platform-okf-bundle)\n\ngithub.com/dirien/pulumi-platform-okf-bundle\n\n](https://github.com/dirien/pulumi-platform-okf-bundle)\n\n## What a standard won’t do for you\n\nThree caveats, so nobody mistakes this for magic.\n\n**It formats knowledge; it doesn’t create it.** The warning from the loop post applies with no edits: memory is what lets a loop compound, and slop compounds right alongside it. A wrong runbook in a beautifully conformant bundle is still a wrong runbook, now served to every agent on the team with confidence. The leverage isn’t the format. It’s that knowledge changes finally go through review like code changes.\n\n**v0.1 is a draft, and the name may not survive.** Google says so itself: the announcement calls v0.1 “a starting point, not a finished standard.” OKF could lose to a better spec next year. I’d start anyway, because converting markdown with frontmatter into markdown with slightly different frontmatter is the cheapest migration you’ll ever run, and it’s precisely the kind of mechanical pass an agent finishes in an afternoon. The shape is the commitment. The name is a detail.\n\n**Conformance is a floor, not hygiene.** The spec requires consumers to tolerate broken links, missing indexes, and unknown types. That tolerance keeps readers working, and it also means nothing forces your bundle to stay healthy. Karpathy’s gist included a lint pass for a reason: contradictions, orphaned pages, stale claims. Keep it, and run it the way you lint code, on a schedule, with findings that reach a human.\n\n## Where to start\n\n1. **Pick the knowledge that already hurts.** The runbook nobody can find during an incident, the onboarding doc that’s wrong in ways only one person knows. One domain, not the whole org.\n2. **Let the agent do the conversion.** Paste the [spec](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) into your coding agent, point it at the existing folder of docs, and have it refactor them into a bundle. This is exactly the bookkeeping LLMs are good at, and it works as well on a wiki export as on a fresh start.\n3. **Put the bundle where the code lives.** Same repo or a sibling, but in git, behind pull requests. The day a knowledge change gets a review comment is the day the bundle becomes trustworthy.\n4. **Wire it into the loop.** Agents read `index.md` first and drill down only when a concept earns it, which keeps the context window small. The memory file I told you to write before the loop now has a format, and every agent you run, in [whatever harness you built](https://www.pulumi.com/blog/stop-tuning-prompts-build-a-harness/), can read the same one.\n\nThe advice from June holds: write the memory file before the loop. What changed is that the file no longer has to be a private dialect. There’s now a one-page, git-native spec for the layer your agents think with, and the smallness that makes it look trivial is the property that lets it spread. The loop does the typing. The wiki does the remembering. And the remembering is now something you can put in a pull request and hand to the next agent, like everything else you ship.\n\n[Wire Pulumi context into your agents](https://www.pulumi.com/docs/ai/)","publishedAt":"2026-07-14T00:00:00.000Z","fetchedAt":"2026-07-15T01:19:34.927Z","url":"https://www.pulumi.com/blog/knowledge-as-code-the-memory-file-just-got-a-spec/","media":[{"type":"image","url":"https://www.pulumi.com/images/generated/blog/knowledge-as-code-the-memory-file-just-got-a-spec/index.png","r2Key":"releases/1101eff54b0488b225d0793887eca73c9fc3a3f07119a062c2bdb216ee43c3b0.png","r2Url":"https://media.releases.sh/releases/1101eff54b0488b225d0793887eca73c9fc3a3f07119a062c2bdb216ee43c3b0.png"},{"type":"image","url":"https://opengraph.githubassets.com/1/dirien/pulumi-platform-okf-bundle","alt":"GitHub repository: dirien/pulumi-platform-okf-bundle","r2Key":"releases/a591a3166155f2f1a5b8e3dc9a2f8955f406dcad64bdcd2d1c1b66a05a238834.png","r2Url":"https://media.releases.sh/releases/a591a3166155f2f1a5b8e3dc9a2f8955f406dcad64bdcd2d1c1b66a05a238834.png"}],"coverageCount":0},{"id":"rel__lVl1_pspFTIGWBuMrWDc","version":null,"type":"feature","title":"Sign in to Pulumi Cloud with Passkeys","summary":"Pulumi Cloud now supports passkeys for users who sign in with email and password, using Touch ID, Face ID, Windows Hello, or hardware keys. Passkeys are phishing-resistant and synced across devices; existing password and MFA configurations remain unchanged.","titleGenerated":"Pulumi Cloud adds passkey support for email-password sign-in","titleShort":"Passkey sign-in for Pulumi Cloud accounts","breaking":"none","importance":3,"content":"![](https://www.pulumi.com/blog/passkey-support-in-pulumi-cloud/meta.png)\n\nPulumi Cloud now supports passkeys for users who sign in with email and password. Select a button, approve with Touch ID, Face ID, Windows Hello, or your hardware key, and you’re signed in.\n\nA passkey is a public-key credential stored on your device: your phone, your laptop, a hardware key (YubiKey, Google Titan, etc.), or your password manager can all function as the authenticator. When you sign in, your device authenticates you locally and signs a challenge from Pulumi Cloud with the private key.\n\nThe private key stays on your device — Pulumi Cloud never sees or stores it. Passkeys are built on the [WebAuthn](https://www.w3.org/TR/webauthn-3/) standard, so they’re already supported on every major browser and operating system.\n\n## Who this is for\n\nThis release applies to users who sign in to Pulumi Cloud with an email address and password. If you sign in through an identity provider (IdP), such as GitHub OAuth, GitLab, Bitbucket, Google, or your organization’s SAML SSO, your existing flow is unchanged.\n\n## Why passkeys\n\nPasswords have always been the weakest link in account security. Since they are shared secrets, they are vulnerable to phishing attacks, and every place you type one is a place that can be impersonated or a data store that can be leaked. Passkeys swap that out for a per-site key pair that lives on your device:\n\n- **Phishing-resistant by design.** A passkey is bound to the exact origin it was registered for. A look-alike domain can’t trigger your authenticator.\n- **Synced across your devices.** Apple iCloud Keychain, Google Password Manager, 1Password, Dashlane, Bitwarden: most credential managers now sync passkeys end-to-end-encrypted to every device you’ve signed in on.\n- **Discoverable.** Pulumi Cloud doesn’t need to know which user you are before you authenticate. Just select “Sign in with a passkey” and your device offers the right credential.\n- **Nothing to remember.** A passkey lives on your device. There’s no string to memorize, and no sensitive credential stored by us.\n\n## Setting up a passkey\n\nThe next time you sign in with a password, Pulumi Cloud will ask if you’d like to configure a passkey. It’s a single-step prompt, so you can enroll once and take advantage of passkeys on every subsequent sign-in.\n\nIf you dismiss the prompt and would like to add one later, or want to add multiple passkeys, navigate to **Account Settings → Passkeys** under your user profile. Select **Register a passkey**, complete the OS-level prompt (Touch ID, Face ID, Windows Hello, or your hardware key), and you’re done. Pulumi Cloud will pick a sensible default name like `\"iCloud Keychain\"` or `\"Chrome on macOS\"` based on the authenticator, but you can rename it inline anytime under **Account Settings → Passkeys**.\n\n![Pulumi Cloud Account Settings page showing the Passkeys section, with a “Register a passkey” button and an enrolled passkey named “Pulumipus’s iPhone”.](https://www.pulumi.com/blog/passkey-support-in-pulumi-cloud/account-settings-passkeys.png)\n\nYou can register as many passkeys as you want. Typical setups are one per personal device, or one synced credential plus a hardware key as backup. Removing a passkey takes effect immediately; deleted credentials cannot be used to sign in.\n\n## Signing in\n\nDepending on how you arrive:\n\n- **Explicit sign-in.** On the sign-in page, select **Sign in with a passkey**. Your browser opens the passkey picker, and you authenticate from there.\n- **Autofill.** If your browser supports [conditional mediation](https://web.dev/articles/passkey-form-autofill) (Chrome, Safari, Edge, recent Firefox), the email field on the sign-in page proactively offers your registered passkeys as autofill suggestions. Pick one and you’re signed in.\n\n## What about my existing password and MFA?\n\nYour existing password still works. Passkeys are an additional sign-in option, not a replacement, and they don’t disable password sign-in on your account. If you lose access to every registered passkey, you can still sign in with your email and password as you always have, then register a new passkey from settings.\n\n**Other 2FA still applies.** If you have TOTP-based MFA enabled on your Pulumi account, a passkey sign-in will still prompt for your second factor. WebAuthn doesn’t reliably tell us *how* you unlocked the passkey on your device (biometric, PIN, or something weaker), so we can’t safely treat the passkey itself as proof of two factors. Your existing MFA configuration remains the boundary it always was.\n\n## Try it out\n\nPasskey support is generally available today for every Pulumi Cloud user who signs in with email and password, at no additional cost and with no configuration. Visit [your account settings](https://app.pulumi.com/account/profile) to register your first passkey, and let us know what you think on [our community Slack](https://slack.pulumi.com/) or [GitHub](https://github.com/pulumi/pulumi/issues).","publishedAt":"2026-07-13T00:00:00.000Z","fetchedAt":"2026-07-15T01:19:34.927Z","url":"https://www.pulumi.com/blog/passkey-support-in-pulumi-cloud/","media":[{"type":"image","url":"https://www.pulumi.com/blog/passkey-support-in-pulumi-cloud/meta.png","r2Key":"releases/c14d3a22f7acdd93ec623966995cece12eb7b510405b910088bd1ce0e7307c0c.png","r2Url":"https://media.releases.sh/releases/c14d3a22f7acdd93ec623966995cece12eb7b510405b910088bd1ce0e7307c0c.png"},{"type":"image","url":"https://www.pulumi.com/blog/passkey-support-in-pulumi-cloud/account-settings-passkeys.png","alt":"Pulumi Cloud Account Settings page showing the Passkeys section, with a “Register a passkey” button and an enrolled passkey named “Pulumipus’s iPhone”.","r2Key":"releases/fa3aa269d704de46e8f81535c2bbed2307e2af7f1e7a0b5af0e7b1a839403399.png","r2Url":"https://media.releases.sh/releases/fa3aa269d704de46e8f81535c2bbed2307e2af7f1e7a0b5af0e7b1a839403399.png"}],"coverageCount":0},{"id":"rel_DywsZ2-PeHOcWfXNXZ-ze","version":null,"type":"feature","title":"New: Versioned CLI and SDK Docs","summary":"The Pulumi CLI command reference and SDK API documentation (Node.js, Python, .NET, Java) now include a version selector dropdown, allowing users to view documentation matching their installed version. Immutable snapshots are available back to v3.150.0.","titleGenerated":"Pulumi documentation now includes version selector for CLI and SDK docs","titleShort":"CLI and SDK docs now versioned back to v3.150.0","breaking":"none","importance":2,"content":"![](https://www.pulumi.com/images/generated/blog/previous-cli-and-sdk-version-docs/index.png)\n\nPinned to an older Pulumi CLI or SDK version and finding that the docs describe a newer release? The [Pulumi CLI command reference](https://www.pulumi.com/docs/iac/cli/commands/) and the SDK API docs now include a version selector, so the documentation you’re reading matches the version you’re actually running.\n\n## How it works\n\nWhen you open the [CLI command reference](https://www.pulumi.com/docs/iac/cli/commands/), you’ll see a version dropdown near the top of the page, below the title. The SDK API docs carry the same dropdown in the upper-right corner. Choose a release, and the page loads the documentation generated for that exact version.\n\n![Version selector near the top of the Pulumi CLI command reference](cli-version-selector.png)\n\n![Version selector in the upper-right corner of the Pulumi Node.js SDK API docs](sdk-version-selector.png)\n\n## What’s available\n\nAlongside the latest release, we keep immutable snapshots of previous versions going back to v3.150.0 (early 2025). The CLI command reference and the Node.js, Python, .NET, and Java SDK API docs are all covered, so the docs you need are only a dropdown away. (The Go SDK is versioned on [pkg.go.dev](https://pkg.go.dev/github.com/pulumi/pulumi/sdk/v3), so its documentation lives there rather than in the Pulumi docs.)\n\n## Get started\n\nHead to the [CLI command reference](https://www.pulumi.com/docs/iac/cli/commands/) or the [SDK API docs](https://www.pulumi.com/docs/reference/) and try the version dropdown. Selections are sticky, so when you navigate into a version, you’ll be able to click around within that same version without having to choose it again.\n\nHave feedback? Let us know in the [Pulumi Community Slack](https://slack.pulumi.com) or by opening an issue on [GitHub](https://github.com/pulumi/docs).\n\n[Browse the CLI command reference](https://www.pulumi.com/docs/iac/cli/commands/)","publishedAt":"2026-07-07T00:00:00.000Z","fetchedAt":"2026-07-08T05:03:36.899Z","url":"https://www.pulumi.com/blog/previous-cli-and-sdk-version-docs/","media":[{"type":"image","url":"https://www.pulumi.com/images/generated/blog/previous-cli-and-sdk-version-docs/index.png","r2Key":"releases/db9ea628cda98168aef2c47dc5665b8c1784df8cc54d9918fa3bf8bfb758f04f.png","r2Url":"https://media.releases.sh/releases/db9ea628cda98168aef2c47dc5665b8c1784df8cc54d9918fa3bf8bfb758f04f.png"}],"coverageCount":0},{"id":"rel_VQuSav1quHs6neFXwJe2j","version":null,"type":"feature","title":"Most Effective Infrastructure as Code (IaC) Tools","summary":"![](https://www.pulumi.com/images/generated/blog/infrastructure-as-code-tools/index.png)\n\nInfrastructure as Code (IaC) has evolved beyond simple autom...","titleGenerated":null,"titleShort":null,"breaking":"unknown","importance":null,"content":"![](https://www.pulumi.com/images/generated/blog/infrastructure-as-code-tools/index.png)\n\nInfrastructure as Code (IaC) has evolved beyond simple automation into a fundamental shift toward applying software engineering practices to infrastructure management. In 2026, leading organizations aren’t just provisioning infrastructure—they’re treating it as software, complete with testing, version control, code reviews, and continuous integration.\n\nAs infrastructure complexity grows, teams increasingly seek approaches that provide the same developer productivity tools they use for application development. While template-based and domain-specific language approaches serve many use cases effectively, teams with complex requirements or programming backgrounds often find that general-purpose programming languages offer advantages in testing, abstraction, and collaboration.\n\nThis comprehensive guide examines the most effective infrastructure as code tools available today, providing detailed analysis of core IaC platforms, complementary tools, and related technologies through the lens of software engineering best practices. Whether you’re starting fresh with IaC or evaluating alternatives to overcome limitations in your current toolchain, we’ll help you navigate this complex landscape and choose solutions that truly bring software engineering to infrastructure.\n\nReady to try one of these approaches yourself? [Get started with Pulumi for free](https://www.pulumi.com/docs/install/) and provision your first resource in minutes, or [see Pulumi Neo in action](https://www.pulumi.com/product/neo/) to explore how an AI agent can manage infrastructure changes alongside your team.\n\n## What is Infrastructure as Code?\n\n[Infrastructure as Code (IaC)](https://www.pulumi.com/what-is/what-is-infrastructure-as-code/) is an approach to automating the provisioning and management of infrastructure using software engineering principles, approaches, and tools. Rather than manually configuring servers, networks, and cloud resources through user interfaces or command-line tools, IaC enables you to define your entire infrastructure declaratively through code.\n\nThis approach brings the same benefits that have revolutionized software development—version control, automated testing, code reviews, and CI/CD pipelines—to infrastructure management.\n\n## What Are Infrastructure as Code Tools?\n\nInfrastructure as Code tools are platforms and frameworks that enable you to define, provision, and manage infrastructure resources through code rather than manual processes. These tools translate your infrastructure definitions into API calls that create, modify, or destroy cloud resources across various providers.\n\nThe most effective IaC tools share several key characteristics:\n\n- **Goal-state focus**: Define your desired infrastructure outcome, whether through declarative syntax (like YAML/JSON templates) or imperative languages that express declarative intent\n- **Multi-cloud support**: Work across different cloud providers and services\n- **State management**: Track the current state of your infrastructure\n- **Preview capabilities**: Show what changes will be made before applying them\n- **Idempotency**: Safe to run multiple times with consistent results\n\n## Why Infrastructure as Code Tools Are Essential\n\nThe shift to IaC tools addresses fundamental challenges that manual infrastructure management cannot solve at scale:\n\n**Accelerates Deployment Velocity**: Teams can provision complex multi-cloud architectures in minutes instead of weeks. This speed enables faster time-to-market and more frequent, reliable deployments.\n\n**Enables True Collaboration**: Infrastructure becomes code that teams can review, test, and approve together. This collaborative approach reduces errors and ensures knowledge sharing across the organization.\n\n**Eliminates Configuration Drift**: Manual changes lead to inconsistencies between environments. IaC ensures your production, staging, and development environments remain identical, eliminating the notorious “works on my machine” syndrome for infrastructure.\n\n**Provides Cost Control**: Automated provisioning and deprovisioning prevents resource sprawl. Teams can easily track infrastructure costs, set budget alerts, and optimize resource usage across environments.\n\n**Ensures Compliance and Security**: Codified security policies and compliance requirements are automatically enforced across all deployments. Audit trails become automatic, and policy violations are caught before deployment.\n\n**Guarantees Business Continuity**: Complete infrastructure definitions stored in version control enable rapid disaster recovery. Organizations can reconstruct entire environments from code, minimizing downtime and data loss.\n\n## Infrastructure as Code Tools Overview\n\nThis guide covers the following infrastructure as code tools and platforms:\n\n### 10 Most Used IaC Tools in 2026\n\n1. **Pulumi IaC** - Modern IaC with general-purpose programming languages\n2. **Terraform** - BSL-licensed IaC from HashiCorp that uses the HCL domain-specific language\n3. **AWS CDK** - Cloud Development Kit for AWS\n4. **AWS CloudFormation** - Native AWS integration\n5. **Azure Resource Manager (ARM)** - Native Azure JSON templates\n6. **Azure Bicep** - Azure-native domain-specific language that compiles to ARM\n7. **Google Cloud Infrastructure Manager** - Terraform-based solution for Google Cloud\n8. **Kubernetes YAML** - Native Kubernetes resource definitions\n9. **Crossplane** - Kubernetes as universal control plane\n10. **OpenTofu** - Open-source Terraform alternative\n\n### Configuration Management Tools\n\n- **Chef** - Configuration management and compliance automation\n- **Puppet** - Configuration management and compliance automation\n- **Salt** - Configuration management and remote execution\n\n### Application and Platform Management Tools\n\n- **Kubernetes** - Container orchestration platform\n\n### Security and Compliance Tools\n\n- **Pulumi ESC** - Configuration and secrets management platform\n- **Pulumi Insights** - Cloud resource search, analytics, and compliance platform\n- **Snyk** - Developer security platform with IaC scanning\n- **Wiz** - Comprehensive cloud security platform\n- **HashiCorp Sentinel** - Policy-as-code framework for Terraform\n- **Checkov** - Static analysis for IaC security\n- **TFLint** - Terraform linting and validation\n\n### Infrastructure Automation and Management Platforms\n\n- **Pulumi Cloud** - Managed service for Pulumi IaC with enterprise features\n- **HashiCorp Cloud Platform** - Enterprise SaaS platform for Terraform management and automation\n- **Spacelift** - Automation platform for IaC workflows (not an IaC tool itself)\n- **Env0** - Governance and automation platform for existing IaC tools\n\n## Core Infrastructure as Code Tools\n\n### 1\\. Pulumi\n\nLicense: Apache 2.0  \nBest For: Teams who want flexible, language-agnostic IaC for infrastructure and operations\n\nPulumi IaC represents a modern approach to infrastructure as code, fundamentally changing how teams approach infrastructure by enabling the use of general-purpose programming languages like Python, TypeScript, Go, C#, and Java, plus YAML for simpler configurations. Unlike tools that force teams to learn proprietary domain-specific languages (DSLs), Pulumi leverages familiar languages and software engineering practices, providing unprecedented flexibility, powerful abstractions, and seamless integration with existing development workflows.\n\nPulumi’s approach combines the best of both imperative and declarative paradigms: you use imperative programming languages to define your desired infrastructure state, but the Pulumi engine processes this declaratively to determine what changes are needed to achieve your intended outcome.\n\n### Key Features:\n\n- **Universal language support**: Use Python, TypeScript, Go, C#, Java, or YAML configurations—no new DSL to learn\n- **Any cloud, any architecture**: Deploy to AWS, Azure, Google Cloud, Kubernetes, and 100+ other providers\n- **Real programming constructs**: Leverage loops, conditionals, functions, classes, packages, and third-party libraries\n- **Superior developer experience**: Full IDE support with IntelliSense, debugging, and refactoring\n- **Built-in testing**: [Unit and integration testing](https://www.pulumi.com/docs/using-pulumi/testing/) for infrastructure code\n- **Policy as Code**: Enforce compliance and security policies with [CrossGuard](https://www.pulumi.com/docs/using-pulumi/crossguard/)\n- **Component ecosystem**: Rich library of reusable infrastructure components\n\nUniversal Language Code Examples:\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">pulumi</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/pulumi\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">aws</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/aws\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">awsx</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/awsx\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Create a VPC with automatic subnets\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">vpc</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">awsx</span><span class=\"p\">.</span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">Vpc</span><span class=\"p\">(</span><span class=\"s2\">\"main-vpc\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">cidrBlock</span><span class=\"o\">:</span> <span class=\"s2\">\"10.0.0.0/16\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">numberOfAvailabilityZones</span>: <span class=\"kt\">2</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Create an ECS cluster\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">cluster</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">ecs</span><span class=\"p\">.</span><span class=\"nx\">Cluster</span><span class=\"p\">(</span><span class=\"s2\">\"app-cluster\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Create an Application Load Balancer\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">alb</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">awsx</span><span class=\"p\">.</span><span class=\"nx\">elasticloadbalancingv2</span><span class=\"p\">.</span><span class=\"nx\">ApplicationLoadBalancer</span><span class=\"p\">(</span><span class=\"s2\">\"app-alb\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">vpcId</span>: <span class=\"kt\">vpc.vpcId</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">subnetIds</span>: <span class=\"kt\">vpc.publicSubnetIds</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Deploy a containerized application\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">service</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">awsx</span><span class=\"p\">.</span><span class=\"nx\">ecs</span><span class=\"p\">.</span><span class=\"nx\">FargateService</span><span class=\"p\">(</span><span class=\"s2\">\"app-service\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">cluster</span>: <span class=\"kt\">cluster.arn</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">taskDefinitionArgs</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">container</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">image</span><span class=\"o\">:</span> <span class=\"s2\">\"nginx:latest\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">memory</span>: <span class=\"kt\">128</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ports</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">containerPort</span>: <span class=\"kt\">80</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">targetGroup</span>: <span class=\"kt\">alb.defaultTargetGroup</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">vpcId</span> <span class=\"o\">=</span> <span class=\"nx\">vpc</span><span class=\"p\">.</span><span class=\"nx\">vpcId</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">serviceUrl</span> <span class=\"o\">=</span> <span class=\"nx\">alb</span><span class=\"p\">.</span><span class=\"nx\">loadBalancer</span><span class=\"p\">.</span><span class=\"nx\">dnsName</span><span class=\"p\">;</span>\n</span></span>\n```\n\n```python\n<span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">pulumi</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">pulumi_aws</span> <span class=\"k\">as</span> <span class=\"nn\">aws</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">pulumi_awsx</span> <span class=\"k\">as</span> <span class=\"nn\">awsx</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Create a VPC with automatic subnets</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">vpc</span> <span class=\"o\">=</span> <span class=\"n\">awsx</span><span class=\"o\">.</span><span class=\"n\">ec2</span><span class=\"o\">.</span><span class=\"n\">Vpc</span><span class=\"p\">(</span><span class=\"s2\">\"main-vpc\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">cidr_block</span><span class=\"o\">=</span><span class=\"s2\">\"10.0.0.0/16\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">number_of_availability_zones</span><span class=\"o\">=</span><span class=\"mi\">2</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Create an ECS cluster</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">cluster</span> <span class=\"o\">=</span> <span class=\"n\">aws</span><span class=\"o\">.</span><span class=\"n\">ecs</span><span class=\"o\">.</span><span class=\"n\">Cluster</span><span class=\"p\">(</span><span class=\"s2\">\"app-cluster\"</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Create an Application Load Balancer</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">alb</span> <span class=\"o\">=</span> <span class=\"n\">awsx</span><span class=\"o\">.</span><span class=\"n\">elasticloadbalancingv2</span><span class=\"o\">.</span><span class=\"n\">ApplicationLoadBalancer</span><span class=\"p\">(</span><span class=\"s2\">\"app-alb\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">vpc_id</span><span class=\"o\">=</span><span class=\"n\">vpc</span><span class=\"o\">.</span><span class=\"n\">vpc_id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">subnet_ids</span><span class=\"o\">=</span><span class=\"n\">vpc</span><span class=\"o\">.</span><span class=\"n\">public_subnet_ids</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Deploy a containerized application</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">service</span> <span class=\"o\">=</span> <span class=\"n\">awsx</span><span class=\"o\">.</span><span class=\"n\">ecs</span><span class=\"o\">.</span><span class=\"n\">FargateService</span><span class=\"p\">(</span><span class=\"s2\">\"app-service\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">cluster</span><span class=\"o\">=</span><span class=\"n\">cluster</span><span class=\"o\">.</span><span class=\"n\">arn</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">task_definition_args</span><span class=\"o\">=</span><span class=\"n\">awsx</span><span class=\"o\">.</span><span class=\"n\">ecs</span><span class=\"o\">.</span><span class=\"n\">FargateServiceTaskDefinitionArgs</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">container</span><span class=\"o\">=</span><span class=\"n\">awsx</span><span class=\"o\">.</span><span class=\"n\">ecs</span><span class=\"o\">.</span><span class=\"n\">TaskDefinitionContainerDefinitionArgs</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">image</span><span class=\"o\">=</span><span class=\"s2\">\"nginx:latest\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">memory</span><span class=\"o\">=</span><span class=\"mi\">128</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">ports</span><span class=\"o\">=</span><span class=\"p\">[</span><span class=\"n\">awsx</span><span class=\"o\">.</span><span class=\"n\">ecs</span><span class=\"o\">.</span><span class=\"n\">TaskDefinitionPortMappingArgs</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">container_port</span><span class=\"o\">=</span><span class=\"mi\">80</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">target_group</span><span class=\"o\">=</span><span class=\"n\">alb</span><span class=\"o\">.</span><span class=\"n\">default_target_group</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">)]</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">))</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">export</span><span class=\"p\">(</span><span class=\"s2\">\"vpc_id\"</span><span class=\"p\">,</span> <span class=\"n\">vpc</span><span class=\"o\">.</span><span class=\"n\">vpc_id</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">export</span><span class=\"p\">(</span><span class=\"s2\">\"service_url\"</span><span class=\"p\">,</span> <span class=\"n\">alb</span><span class=\"o\">.</span><span class=\"n\">load_balancer</span><span class=\"o\">.</span><span class=\"n\">dns_name</span><span class=\"p\">)</span>\n</span></span>\n```\n\n```go\n<span class=\"line\"><span class=\"cl\"><span class=\"kn\">package</span><span class=\"w\"> </span><span class=\"nx\">main</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ecs\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"github.com/pulumi/pulumi-awsx/sdk/v2/go/awsx/ec2\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"github.com/pulumi/pulumi-awsx/sdk/v2/go/awsx/elasticloadbalancingv2\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">awsxecs</span><span class=\"w\"> </span><span class=\"s\">\"github.com/pulumi/pulumi-awsx/sdk/v2/go/awsx/ecs\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">func</span><span class=\"w\"> </span><span class=\"nf\">main</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">Run</span><span class=\"p\">(</span><span class=\"kd\">func</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"w\"> </span><span class=\"o\">*</span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">Context</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"kt\">error</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c1\">// Create a VPC with automatic subnets</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">vpc</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nf\">NewVpc</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"main-vpc\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"o\">&</span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">VpcArgs</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">CidrBlock</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">String</span><span class=\"p\">(</span><span class=\"s\">\"10.0.0.0/16\"</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">NumberOfAvailabilityZones</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">Int</span><span class=\"p\">(</span><span class=\"mi\">2</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">})</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">!=</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c1\">// Create an ECS cluster</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">cluster</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"nx\">ecs</span><span class=\"p\">.</span><span class=\"nf\">NewCluster</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"app-cluster\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">!=</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c1\">// Create an Application Load Balancer</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">alb</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"nx\">elasticloadbalancingv2</span><span class=\"p\">.</span><span class=\"nf\">NewApplicationLoadBalancer</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"app-alb\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"o\">&</span><span class=\"nx\">elasticloadbalancingv2</span><span class=\"p\">.</span><span class=\"nx\">ApplicationLoadBalancerArgs</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">VpcId</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">vpc</span><span class=\"p\">.</span><span class=\"nx\">VpcId</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">SubnetIds</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">vpc</span><span class=\"p\">.</span><span class=\"nx\">PublicSubnetIds</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">})</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">!=</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c1\">// Deploy a containerized application</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">_</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"p\">=</span><span class=\"w\"> </span><span class=\"nx\">awsxecs</span><span class=\"p\">.</span><span class=\"nf\">NewFargateService</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"app-service\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"o\">&</span><span class=\"nx\">awsxecs</span><span class=\"p\">.</span><span class=\"nx\">FargateServiceArgs</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">Cluster</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">cluster</span><span class=\"p\">.</span><span class=\"nx\">Arn</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">TaskDefinitionArgs</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"o\">&</span><span class=\"nx\">awsxecs</span><span class=\"p\">.</span><span class=\"nx\">FargateServiceTaskDefinitionArgs</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">Container</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"o\">&</span><span class=\"nx\">awsxecs</span><span class=\"p\">.</span><span class=\"nx\">TaskDefinitionContainerDefinitionArgs</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">Image</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">String</span><span class=\"p\">(</span><span class=\"s\">\"nginx:latest\"</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">Memory</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">Int</span><span class=\"p\">(</span><span class=\"mi\">128</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">Ports</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">awsxecs</span><span class=\"p\">.</span><span class=\"nx\">TaskDefinitionPortMappingArray</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"o\">&</span><span class=\"nx\">awsxecs</span><span class=\"p\">.</span><span class=\"nx\">TaskDefinitionPortMappingArgs</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">ContainerPort</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">Int</span><span class=\"p\">(</span><span class=\"mi\">80</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">TargetGroup</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">alb</span><span class=\"p\">.</span><span class=\"nx\">DefaultTargetGroup</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">},</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">},</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">},</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">},</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">})</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">!=</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">ctx</span><span class=\"p\">.</span><span class=\"nf\">Export</span><span class=\"p\">(</span><span class=\"s\">\"vpcId\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">vpc</span><span class=\"p\">.</span><span class=\"nx\">VpcId</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">ctx</span><span class=\"p\">.</span><span class=\"nf\">Export</span><span class=\"p\">(</span><span class=\"s\">\"serviceUrl\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">alb</span><span class=\"p\">.</span><span class=\"nx\">LoadBalancer</span><span class=\"p\">.</span><span class=\"nf\">DnsName</span><span class=\"p\">())</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">})</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span><span class=\"w\">\n</span></span></span>\n```\n\n```csharp\n<span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">System.Collections.Generic</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">Pulumi</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">Aws</span> <span class=\"p\">=</span> <span class=\"n\">Pulumi</span><span class=\"p\">.</span><span class=\"n\">Aws</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">Awsx</span> <span class=\"p\">=</span> <span class=\"n\">Pulumi</span><span class=\"p\">.</span><span class=\"n\">Awsx</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">return</span> <span class=\"k\">await</span> <span class=\"n\">Deployment</span><span class=\"p\">.</span><span class=\"n\">RunAsync</span><span class=\"p\">(()</span> <span class=\"p\">=></span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Create a VPC with automatic subnets</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"n\">vpc</span> <span class=\"p\">=</span> <span class=\"k\">new</span> <span class=\"n\">Awsx</span><span class=\"p\">.</span><span class=\"n\">Ec2</span><span class=\"p\">.</span><span class=\"n\">Vpc</span><span class=\"p\">(</span><span class=\"s\">\"main-vpc\"</span><span class=\"p\">,</span> <span class=\"k\">new</span><span class=\"p\">()</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">CidrBlock</span> <span class=\"p\">=</span> <span class=\"s\">\"10.0.0.0/16\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">NumberOfAvailabilityZones</span> <span class=\"p\">=</span> <span class=\"m\">2</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Create an ECS cluster</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"n\">cluster</span> <span class=\"p\">=</span> <span class=\"k\">new</span> <span class=\"n\">Aws</span><span class=\"p\">.</span><span class=\"n\">Ecs</span><span class=\"p\">.</span><span class=\"n\">Cluster</span><span class=\"p\">(</span><span class=\"s\">\"app-cluster\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Create an Application Load Balancer</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"n\">alb</span> <span class=\"p\">=</span> <span class=\"k\">new</span> <span class=\"n\">Awsx</span><span class=\"p\">.</span><span class=\"n\">ElasticLoadBalancingV2</span><span class=\"p\">.</span><span class=\"n\">ApplicationLoadBalancer</span><span class=\"p\">(</span><span class=\"s\">\"app-alb\"</span><span class=\"p\">,</span> <span class=\"k\">new</span><span class=\"p\">()</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">VpcId</span> <span class=\"p\">=</span> <span class=\"n\">vpc</span><span class=\"p\">.</span><span class=\"n\">VpcId</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">SubnetIds</span> <span class=\"p\">=</span> <span class=\"n\">vpc</span><span class=\"p\">.</span><span class=\"n\">PublicSubnetIds</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Deploy a containerized application</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"n\">service</span> <span class=\"p\">=</span> <span class=\"k\">new</span> <span class=\"n\">Awsx</span><span class=\"p\">.</span><span class=\"n\">Ecs</span><span class=\"p\">.</span><span class=\"n\">FargateService</span><span class=\"p\">(</span><span class=\"s\">\"app-service\"</span><span class=\"p\">,</span> <span class=\"k\">new</span><span class=\"p\">()</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">Cluster</span> <span class=\"p\">=</span> <span class=\"n\">cluster</span><span class=\"p\">.</span><span class=\"n\">Arn</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">TaskDefinitionArgs</span> <span class=\"p\">=</span> <span class=\"k\">new</span> <span class=\"n\">Awsx</span><span class=\"p\">.</span><span class=\"n\">Ecs</span><span class=\"p\">.</span><span class=\"n\">Inputs</span><span class=\"p\">.</span><span class=\"n\">FargateServiceTaskDefinitionArgs</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">Container</span> <span class=\"p\">=</span> <span class=\"k\">new</span> <span class=\"n\">Awsx</span><span class=\"p\">.</span><span class=\"n\">Ecs</span><span class=\"p\">.</span><span class=\"n\">Inputs</span><span class=\"p\">.</span><span class=\"n\">TaskDefinitionContainerDefinitionArgs</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">Image</span> <span class=\"p\">=</span> <span class=\"s\">\"nginx:latest\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">Memory</span> <span class=\"p\">=</span> <span class=\"m\">128</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">Ports</span> <span class=\"p\">=</span> <span class=\"k\">new</span><span class=\"p\">[]</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">new</span> <span class=\"n\">Awsx</span><span class=\"p\">.</span><span class=\"n\">Ecs</span><span class=\"p\">.</span><span class=\"n\">Inputs</span><span class=\"p\">.</span><span class=\"n\">TaskDefinitionPortMappingArgs</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">ContainerPort</span> <span class=\"p\">=</span> <span class=\"m\">80</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">TargetGroup</span> <span class=\"p\">=</span> <span class=\"n\">alb</span><span class=\"p\">.</span><span class=\"n\">DefaultTargetGroup</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">return</span> <span class=\"k\">new</span> <span class=\"n\">Dictionary</span><span class=\"p\"><</span><span class=\"kt\">string</span><span class=\"p\">,</span> <span class=\"kt\">object?</span><span class=\"p\">></span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"na\"> [\"vpcId\"]</span> <span class=\"p\">=</span> <span class=\"n\">vpc</span><span class=\"p\">.</span><span class=\"n\">VpcId</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"na\"> [\"serviceUrl\"]</span> <span class=\"p\">=</span> <span class=\"n\">alb</span><span class=\"p\">.</span><span class=\"n\">LoadBalancer</span><span class=\"p\">.</span><span class=\"n\">Apply</span><span class=\"p\">(</span><span class=\"n\">lb</span> <span class=\"p\">=></span> <span class=\"n\">lb</span><span class=\"p\">.</span><span class=\"n\">DnsName</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">};</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">aws-ecs-example</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">runtime</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">yaml</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">description</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">An example that deploys an ECS Fargate service with load balancer</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">resources</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Create a VPC with automatic subnets</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">main-vpc</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">awsx:ec2:Vpc</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">properties</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">cidrBlock</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"10.0.0.0/16\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">numberOfAvailabilityZones</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">2</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Create an ECS cluster</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">app-cluster</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">aws:ecs:Cluster</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Create an Application Load Balancer</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">app-alb</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">awsx:elasticloadbalancingv2:ApplicationLoadBalancer</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">properties</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">vpcId</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${main-vpc.vpcId}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">subnetIds</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${main-vpc.publicSubnetIds}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Deploy a containerized application</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">app-service</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">awsx:ecs:FargateService</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">properties</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">cluster</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${app-cluster.arn}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">taskDefinitionArgs</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">container</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">image</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"nginx:latest\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">memory</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">128</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ports</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">containerPort</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">80</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">targetGroup</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${app-alb.defaultTargetGroup}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">outputs</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">vpcId</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${main-vpc.vpcId}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">serviceUrl</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${app-alb.loadBalancer.dnsName}</span><span class=\"w\">\n</span></span></span>\n```\n\nKey Features:\n\n- **General-purpose language support**: Use Python, TypeScript, Go, C#, Java, or YAML without learning new DSLs\n- **Software engineering practices**: Full IDE support, comprehensive testing frameworks, debugging capabilities\n- **Multi-cloud flexibility**: Native cloud provider SDKs with same-day feature access across [150+ providers](https://www.pulumi.com/registry/)\n- **Incremental adoption**: Migration tools and state integration for gradual transitions\n- **Open source licensing**: Apache 2.0 ensures long-term freedom and flexibility\n\nConsiderations:\n\n- **Learning curve**: Teams new to programming may prefer template-based approaches initially\n- **Ecosystem maturity**: Smaller community compared to more established tools like Terraform\n- **Tool complexity**: Advanced features may require more setup than simpler template systems\n\nOrganizations like Unity, Snowflake, and Starburst have reported significant productivity improvements (80-90% deployment time reductions) when adopting programming language-based approaches. These improvements typically occur when transitioning from manual processes or basic template systems to automated approaches with comprehensive testing, IDE integration, and code reusability. Results vary based on starting point, team expertise, infrastructure complexity, and specific use cases.\n\n> **Ready to get started?** [Experience Pulumi’s programming language approach](https://www.pulumi.com/docs/get-started/) and see how familiar languages can transform your infrastructure management with comprehensive testing, powerful abstractions, and seamless multi-cloud support.\n\n### 2\\. Terraform\n\nLicense: Business Source License (BSL) 1.1 (Not Open Source)  \nBest For: Teams with existing Terraform expertise and established workflows\n\n[Terraform](https://www.pulumi.com/docs/iac/comparisons/terraform/) uses HashiCorp Configuration Language (HCL) to define infrastructure across multiple cloud providers. However, its 2023 licensing change to BSL (no longer open source) and inherent limitations with domain-specific languages create challenges for teams requiring advanced software engineering practices.\n\nKey Features:\n\n- **Extensive provider ecosystem**: Largest collection of community-maintained providers covering virtually every cloud service\n- **Established workflows**: Mature plan-and-apply process with extensive tooling and CI/CD integrations\n- **Community and resources**: Vast ecosystem of modules, extensive documentation, training materials, and community support\n- **Enterprise adoption**: Widely adopted in enterprises with established processes and expertise\n- **Module ecosystem**: Rich collection of reusable Terraform modules for common patterns\n\nWhen Terraform Works Well:\n\n- Teams with existing HCL expertise and Terraform investments\n- Infrastructure patterns that fit well within HCL’s declarative model\n- Teams comfortable with template-based approaches\n\nConsiderations for Complex Scenarios:\n\n- **Language constraints**: HCL’s domain-specific nature can require workarounds for complex logic compared to general-purpose programming languages\n- **Testing approach**: Primarily supports integration testing; teams needing comprehensive unit testing may require additional tooling\n- **IDE experience**: While improving, HCL tooling provides less comprehensive support than mature programming language ecosystems\n- **State management**: Requires manual backend configuration and locking setup for team collaboration\n\nCode Example:\n\n```hcl\n<span class=\"line\"><span class=\"cl\"><span class=\"k\">data</span> <span class=\"s2\">\"aws_availability_zones\" \"available\"</span> {}\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">resource</span> <span class=\"s2\">\"aws_vpc\" \"main\"</span> {\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> cidr_block</span> <span class=\"o\">=</span> <span class=\"s2\">\"10.0.0.0/16\"</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> tags</span> <span class=\"o\">=</span> {\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> Name</span> <span class=\"o\">=</span> <span class=\"s2\">\"main-vpc\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> }\n</span></span><span class=\"line\"><span class=\"cl\">}\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">resource</span> <span class=\"s2\">\"aws_subnet\" \"public\"</span> {\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> count</span> <span class=\"o\">=</span> <span class=\"m\">2</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> vpc_id</span> <span class=\"o\">=</span> <span class=\"k\">aws_vpc</span><span class=\"p\">.</span><span class=\"k\">main</span><span class=\"p\">.</span><span class=\"k\">id</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> cidr_block</span> <span class=\"o\">=</span> <span class=\"s2\">\"10.0.${count.index + 1}.0/24\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> availability_zone</span> <span class=\"o\">=</span> <span class=\"k\">data</span><span class=\"p\">.</span><span class=\"k\">aws_availability_zones</span><span class=\"p\">.</span><span class=\"k\">available</span><span class=\"p\">.</span><span class=\"k\">names</span><span class=\"p\">[</span><span class=\"k\">count</span><span class=\"p\">.</span><span class=\"k\">index</span><span class=\"p\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> tags</span> <span class=\"o\">=</span> {\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> Name</span> <span class=\"o\">=</span> <span class=\"s2\">\"public-subnet-${count.index + 1}\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> }\n</span></span><span class=\"line\"><span class=\"cl\">}\n</span></span>\n```\n\n### 3\\. AWS Cloud Development Kit (CDK)\n\nLicense: Apache 2.0  \nBest For: AWS-focused teams who prefer programming languages over templates\n\nAWS CDK allows you to define AWS infrastructure using familiar programming languages, synthesizing CloudFormation templates for deployment while providing higher-level abstractions. CDK addresses many limitations of traditional template-based approaches by enabling general-purpose programming languages.\n\nKey Features:\n\n- **General-purpose programming languages**: TypeScript, Python, Java, C#, JavaScript support with full IDE integration (Go available in Developer Preview)\n- **AWS-optimized constructs**: High-level components encapsulating AWS best practices\n- **Type safety**: Compile-time checking and IntelliSense support\n- **CloudFormation reliability**: Built on AWS’s proven deployment engine\n\nNotable Limitations:\n\n- **AWS-only ecosystem**: Locked into single cloud provider, limiting multi-cloud strategies\n- **CloudFormation constraints**: Inherits template size limits and deployment restrictions\n- **Vendor lock-in**: Deep AWS integration makes migration to other clouds challenging\n- **Limited cross-cloud consistency**: Teams need different tools and approaches for multi-cloud deployments\n\nCDK represents a significant improvement over CloudFormation templates but constrains organizations to AWS-only infrastructure strategies.\n\nCode Example:\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">cdk</span> <span class=\"kr\">from</span> <span class=\"s1\">'aws-cdk-lib'</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">ec2</span> <span class=\"kr\">from</span> <span class=\"s1\">'aws-cdk-lib/aws-ec2'</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">ecs</span> <span class=\"kr\">from</span> <span class=\"s1\">'aws-cdk-lib/aws-ecs'</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"p\">{</span> <span class=\"nx\">Construct</span> <span class=\"p\">}</span> <span class=\"kr\">from</span> <span class=\"s1\">'constructs'</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">class</span> <span class=\"nx\">MyStack</span> <span class=\"kr\">extends</span> <span class=\"nx\">cdk</span><span class=\"p\">.</span><span class=\"nx\">Stack</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">constructor</span><span class=\"p\">(</span><span class=\"nx\">scope</span>: <span class=\"kt\">Construct</span><span class=\"p\">,</span> <span class=\"nx\">id</span>: <span class=\"kt\">string</span><span class=\"p\">,</span> <span class=\"nx\">props?</span>: <span class=\"kt\">cdk.StackProps</span><span class=\"p\">)</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">super</span><span class=\"p\">(</span><span class=\"nx\">scope</span><span class=\"p\">,</span> <span class=\"nx\">id</span><span class=\"p\">,</span> <span class=\"nx\">props</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">const</span> <span class=\"nx\">vpc</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">Vpc</span><span class=\"p\">(</span><span class=\"k\">this</span><span class=\"p\">,</span> <span class=\"s1\">'MyVpc'</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">maxAzs</span>: <span class=\"kt\">2</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">const</span> <span class=\"nx\">cluster</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">ecs</span><span class=\"p\">.</span><span class=\"nx\">Cluster</span><span class=\"p\">(</span><span class=\"k\">this</span><span class=\"p\">,</span> <span class=\"s1\">'MyCluster'</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">vpc</span>: <span class=\"kt\">vpc</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">const</span> <span class=\"nx\">taskDefinition</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">ecs</span><span class=\"p\">.</span><span class=\"nx\">FargateTaskDefinition</span><span class=\"p\">(</span><span class=\"k\">this</span><span class=\"p\">,</span> <span class=\"s1\">'TaskDef'</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">memoryLimitMiB</span>: <span class=\"kt\">512</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">cpu</span>: <span class=\"kt\">256</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">taskDefinition</span><span class=\"p\">.</span><span class=\"nx\">addContainer</span><span class=\"p\">(</span><span class=\"s1\">'web'</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">image</span>: <span class=\"kt\">ecs.ContainerImage.fromRegistry</span><span class=\"p\">(</span><span class=\"s1\">'nginx:latest'</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">portMappings</span><span class=\"o\">:</span> <span class=\"p\">[{</span> <span class=\"nx\">containerPort</span>: <span class=\"kt\">80</span> <span class=\"p\">}]</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">new</span> <span class=\"nx\">ecs</span><span class=\"p\">.</span><span class=\"nx\">FargateService</span><span class=\"p\">(</span><span class=\"k\">this</span><span class=\"p\">,</span> <span class=\"s1\">'MyService'</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">cluster</span>: <span class=\"kt\">cluster</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">taskDefinition</span>: <span class=\"kt\">taskDefinition</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span>\n```\n\n### 4\\. AWS CloudFormation\n\nLicense: Proprietary (AWS Service)  \nBest For: AWS-only deployments requiring deep service integration\n\nAWS CloudFormation provides the foundation for infrastructure as code on AWS, offering native integration with all AWS services and deep platform-specific features.\n\nPulumi Integration: Pulumi provides [native AWS providers](https://www.pulumi.com/docs/clouds/aws/) that offer the same comprehensive AWS service coverage as CloudFormation, with the added benefit of using general-purpose programming languages. You can also [import existing CloudFormation stacks](https://www.pulumi.com/docs/using-pulumi/adopting-pulumi/import/) into Pulumi for gradual migration or hybrid management approaches.\n\nKey Features:\n\n- **AWS-native**: First-party support for all AWS services\n- **JSON/YAML templates**: Declarative resource definitions\n- **Stack management**: Organized resource grouping and lifecycle management\n- **Change sets**: Preview infrastructure changes before deployment\n- **Service integration**: Deep integration with other AWS services\n\nCode Example:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"nt\">AWSTemplateFormatVersion</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s1\">'2010-09-09'</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">Resources</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">MyVPC</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">Type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">AWS::EC2::VPC</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">Properties</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">CidrBlock</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">10.0.0.0</span><span class=\"l\">/16</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">EnableDnsHostnames</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kc\">true</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">Tags</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">Key</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">Name</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">Value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">MyVPC</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">MySubnet</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">Type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">AWS::EC2::Subnet</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">Properties</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">VpcId</span><span class=\"p\">:</span><span class=\"w\"> </span>!<span class=\"l\">Ref MyVPC</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">CidrBlock</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">10.0.1.0</span><span class=\"l\">/24</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">AvailabilityZone</span><span class=\"p\">:</span><span class=\"w\"> </span>!<span class=\"l\">Select [0, !GetAZs '']</span><span class=\"w\">\n</span></span></span>\n```\n\n### 5\\. Azure Resource Manager (ARM)\n\nLicense: Proprietary (Microsoft Service)  \nBest For: Azure-native deployments requiring comprehensive platform integration\n\nAzure Resource Manager provides the foundational infrastructure as code solution for Microsoft Azure, offering complete support for Azure services through JSON-based ARM templates. As Azure’s native IaC solution, ARM templates provide the most comprehensive coverage of Azure services and features.\n\nPulumi Integration: Pulumi’s [native Azure providers](https://www.pulumi.com/docs/clouds/azure/) offer equivalent comprehensive Azure service coverage with general-purpose programming languages. ARM templates can be [imported into Pulumi](https://www.pulumi.com/docs/using-pulumi/adopting-pulumi/import/), and you can reference ARM deployments from Pulumi programs for hybrid scenarios.\n\nKey Features:\n\n- **Azure-native**: Complete Azure service coverage with first-party support\n- **JSON templates**: Declarative resource definitions in JSON format\n- **Resource groups**: Logical organization of related resources\n- **Deployment modes**: Complete or incremental deployment options\n- **Comprehensive integration**: Deep integration with Azure services and features\n\nCode Example:\n\n```json\n<span class=\"line\"><span class=\"cl\"><span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"$schema\"</span><span class=\"p\">:</span> <span class=\"s2\">\"https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"contentVersion\"</span><span class=\"p\">:</span> <span class=\"s2\">\"1.0.0.0\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"resources\"</span><span class=\"p\">:</span> <span class=\"p\">[</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"type\"</span><span class=\"p\">:</span> <span class=\"s2\">\"Microsoft.Storage/storageAccounts\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"apiVersion\"</span><span class=\"p\">:</span> <span class=\"s2\">\"2023-01-01\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"name\"</span><span class=\"p\">:</span> <span class=\"s2\">\"mystorageaccount\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"location\"</span><span class=\"p\">:</span> <span class=\"s2\">\"[resourceGroup().location]\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"sku\"</span><span class=\"p\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"name\"</span><span class=\"p\">:</span> <span class=\"s2\">\"Standard_LRS\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"kind\"</span><span class=\"p\">:</span> <span class=\"s2\">\"StorageV2\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span>\n```\n\n### 6\\. Azure Bicep\n\nLicense: MIT  \nBest For: Azure deployments requiring improved readability and developer experience\n\nAzure Bicep is a domain-specific language (DSL) that simplifies Azure Resource Manager template authoring. Bicep files compile transparently to ARM templates, providing all the capabilities of ARM with significantly improved syntax and developer experience.\n\nKey Features:\n\n- **Simplified syntax**: Clean, readable syntax compared to JSON ARM templates\n- **ARM compilation**: Compiles to ARM templates for deployment, ensuring full compatibility\n- **Type safety**: Strong typing and IntelliSense support in development environments\n- **Modular design**: Support for modules and code reuse across projects\n- **Azure-native**: Complete Azure service coverage through ARM template compilation\n\nCode Example:\n\n```bicep\n<span class=\"line\"><span class=\"cl\"><span class=\"p\">@</span><span class=\"nf\">description</span><span class=\"p\">(</span><span class=\"s\">'Storage Account type'</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">@</span><span class=\"nf\">allowed</span><span class=\"p\">([</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">'Standard_LRS'</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">'Standard_GRS'</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">'Standard_ZRS'</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">'Premium_LRS'</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">])</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">param</span><span class=\"w\"> </span><span class=\"nv\">storageAccountType</span><span class=\"w\"> </span><span class=\"nv\">string</span><span class=\"w\"> </span><span class=\"p\">=</span><span class=\"w\"> </span><span class=\"s\">'Standard_LRS'</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">@</span><span class=\"nf\">description</span><span class=\"p\">(</span><span class=\"s\">'Location for the storage account.'</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">param</span><span class=\"w\"> </span><span class=\"nv\">location</span><span class=\"w\"> </span><span class=\"nv\">string</span><span class=\"w\"> </span><span class=\"p\">=</span><span class=\"w\"> </span><span class=\"nf\">resourceGroup</span><span class=\"p\">().</span><span class=\"nv\">location</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">resource</span><span class=\"w\"> </span><span class=\"nv\">storageAccount</span><span class=\"w\"> </span><span class=\"s\">'Microsoft.Storage/storageAccounts@2023-01-01'</span><span class=\"w\"> </span><span class=\"p\">=</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nv\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s\">'mystorageaccount'</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nv\">location</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nv\">location</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nv\">sku</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nv\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nv\">storageAccountType</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nv\">kind</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s\">'StorageV2'</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nv\">properties</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"p\">{}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span><span class=\"w\">\n</span></span></span>\n```\n\n### 7\\. Google Cloud Infrastructure Manager\n\nLicense: Proprietary (Google Service)  \nBest For: Google Cloud Platform deployments using Terraform\n\nGoogle Cloud Infrastructure Manager automates the deployment and management of Google Cloud infrastructure resources using Terraform configurations, representing Google’s modern approach to infrastructure as code. Infrastructure Manager replaces Google Cloud Deployment Manager, which reached end of support on December 31, 2025.\n\nKey Features:\n\n- **Terraform-based**: Uses standard Terraform configurations declaratively\n- **Automated workflows**: Handles Terraform init, validate, and apply operations\n- **Version control integration**: Supports Git repositories and Cloud Storage\n- **Deployment tracking**: Comprehensive metadata storage and logging\n- **Multiple Terraform versions**: Flexibility in Terraform version selection\n- **Cloud Build integration**: Leverages Google Cloud Build for execution environment\n- **Migration path**: Provides upgrade path from legacy Cloud Deployment Manager\n\nCode Example:\n\n```hcl\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\"># main.tf - Terraform configuration for Infrastructure Manager\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">resource</span> <span class=\"s2\">\"google_compute_instance\" \"vm_instance\"</span> {\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> name</span> <span class=\"o\">=</span> <span class=\"s2\">\"my-vm\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> machine_type</span> <span class=\"o\">=</span> <span class=\"s2\">\"e2-medium\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> zone</span> <span class=\"o\">=</span> <span class=\"s2\">\"us-central1-a\"</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">boot_disk</span> {\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">initialize_params</span> {\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> image</span> <span class=\"o\">=</span> <span class=\"s2\">\"debian-cloud/debian-11\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> }\n</span></span><span class=\"line\"><span class=\"cl\"> }\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">network_interface</span> {\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> network</span> <span class=\"o\">=</span> <span class=\"s2\">\"default\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">access_config</span> {\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"err\">//</span> <span class=\"k\">Ephemeral</span> <span class=\"k\">public</span> <span class=\"k\">IP</span>\n</span></span><span class=\"line\"><span class=\"cl\"> }\n</span></span><span class=\"line\"><span class=\"cl\"> }\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> metadata</span> <span class=\"o\">=</span> {\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> startup-script</span> <span class=\"o\">=</span> <span class=\"s2\">\"echo Hello from Infrastructure Manager!\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> }\n</span></span><span class=\"line\"><span class=\"cl\">}\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">output</span> <span class=\"s2\">\"instance_ip\"</span> {\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> value</span> <span class=\"o\">=</span> <span class=\"k\">google_compute_instance</span><span class=\"p\">.</span><span class=\"k\">vm_instance</span><span class=\"p\">.</span><span class=\"k\">network_interface</span><span class=\"p\">[</span><span class=\"m\">0</span><span class=\"p\">].</span><span class=\"k\">access_config</span><span class=\"p\">[</span><span class=\"m\">0</span><span class=\"p\">].</span><span class=\"k\">nat_ip</span>\n</span></span><span class=\"line\"><span class=\"cl\">}\n</span></span>\n```\n\n### 8\\. Kubernetes YAML\n\nLicense: Apache 2.0  \nBest For: Teams managing container-native applications and cloud-native infrastructure\n\nKubernetes YAML manifests represent one of the most widely adopted forms of infrastructure as code, enabling teams to define, version, and manage containerized applications and their supporting infrastructure through declarative configuration files.\n\nKey Features:\n\n- **Declarative configuration**: Define desired state through human-readable YAML files\n- **Native Kubernetes integration**: Direct integration with Kubernetes API without additional tools\n- **GitOps compatibility**: Version control YAML files for automated deployment workflows\n- **Resource relationships**: Define dependencies and relationships between Kubernetes resources\n- **Extensive ecosystem**: Rich ecosystem of operators and custom resources extending functionality\n\nCode Example:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"nt\">apiVersion</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">apps/v1</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">kind</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">Deployment</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">metadata</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">web-app</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">labels</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">app</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">web-app</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">spec</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">replicas</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">3</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">selector</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">matchLabels</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">app</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">web-app</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">template</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">metadata</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">labels</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">app</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">web-app</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">spec</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">containers</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">web-app</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">image</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">nginx:1.21</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ports</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">containerPort</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">80</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">resources</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">requests</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">memory</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"64Mi\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">cpu</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"250m\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">limits</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">memory</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"128Mi\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">cpu</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"500m\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nn\">---</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">apiVersion</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">v1</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">kind</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">Service</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">metadata</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">web-app-service</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">spec</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">selector</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">app</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">web-app</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ports</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">port</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">80</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">targetPort</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">80</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">LoadBalancer</span><span class=\"w\">\n</span></span></span>\n```\n\n### 9\\. Crossplane\n\nLicense: Apache 2.0  \nBest For: Kubernetes-first organizations managing multi-cloud infrastructure\n\nCrossplane is a Cloud-Native Framework for Platform Engineering that extends Kubernetes to help organizations build custom infrastructure management platforms, allowing teams to provision and manage cloud resources using Kubernetes APIs and patterns.\n\nPulumi Integration: Pulumi offers the [Pulumi Kubernetes Operator (PKO)](https://www.pulumi.com/docs/using-pulumi/continuous-delivery/pulumi-kubernetes-operator/) that provides similar Kubernetes-native infrastructure management capabilities, plus support for YAML-based definitions. Teams can also use Pulumi programs to provision the underlying infrastructure that Crossplane manages, creating layered infrastructure management approaches.\n\nKey Features:\n\n- **Kubernetes-native**: Uses CRDs and standard Kubernetes patterns\n- **Composite resources**: Create higher-level infrastructure abstractions\n- **GitOps compatibility**: Seamless integration with GitOps workflows\n- **Multi-cloud support**: Provision resources across cloud providers\n- **Policy integration**: Leverage Kubernetes RBAC and admission controllers\n\nCode Example:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"nt\">apiVersion</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">ec2.aws.crossplane.io/v1alpha1</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">kind</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">VPC</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">metadata</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">sample-vpc</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">spec</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">cidrBlock</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">10.0.0.0</span><span class=\"l\">/16</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">region</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">us-east-1</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">tags</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">Name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">sample-vpc</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">providerConfigRef</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">aws-provider-config</span><span class=\"w\">\n</span></span></span>\n```\n\n### 10\\. OpenTofu\n\nLicense: Mozilla Public License 2.0  \nBest For: Teams seeking an open-source Terraform alternative with community governance\n\nOpenTofu emerged as a fork of Terraform v1.5.x following HashiCorp’s license change, maintained by the Linux Foundation. It provides [full compatibility with Terraform](https://www.pulumi.com/docs/iac/concepts/vs/opentofu/) while ensuring long-term open-source availability under MPL 2.0 licensing.\n\nKey Features:\n\n- **True open source**: MPL 2.0 license with community governance via Linux Foundation ensuring long-term accessibility\n- **Terraform compatibility**: Drop-in replacement maintaining existing workflows, modules, and provider ecosystem\n- **Community-driven development**: Transparent roadmap, open contribution process, and vendor-neutral governance\n- **License certainty**: Removes concerns about future licensing restrictions for commercial use\n\nWhen OpenTofu Makes Sense:\n\n- Organizations requiring guaranteed open-source licensing for compliance or philosophical reasons\n- Teams with significant Terraform investments wanting to avoid vendor lock-in\n- Environments where community governance and transparency are priorities\n- Projects needing long-term license stability\n\nArchitectural Considerations: As a Terraform fork, OpenTofu inherits the same architectural approach, which means teams evaluating it should consider whether HCL-based infrastructure definition meets their long-term needs for testing, IDE integration, and developer productivity, or whether a programming language-based approach might better serve complex scenarios.\n\nCode Example:\n\n```hcl\n<span class=\"line\"><span class=\"cl\"><span class=\"k\">resource</span> <span class=\"s2\">\"aws_instance\" \"web\"</span> {\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> ami</span> <span class=\"o\">=</span> <span class=\"s2\">\"ami-0c55b159cbfafe1d0\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> instance_type</span> <span class=\"o\">=</span> <span class=\"s2\">\"t3.micro\"</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> tags</span> <span class=\"o\">=</span> {\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> Name</span> <span class=\"o\">=</span> <span class=\"s2\">\"HelloWorld\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> }\n</span></span><span class=\"line\"><span class=\"cl\">}\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">output</span> <span class=\"s2\">\"instance_ip\"</span> {\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\"> value</span> <span class=\"o\">=</span> <span class=\"k\">aws_instance</span><span class=\"p\">.</span><span class=\"k\">web</span><span class=\"p\">.</span><span class=\"k\">public_ip</span>\n</span></span><span class=\"line\"><span class=\"cl\">}\n</span></span>\n```\n\n### Ansible\n\nLicense: GPL v3  \nBest For: Configuration management with some infrastructure provisioning capabilities\n\nAnsible is primarily a configuration management tool, not a pure Infrastructure as Code tool. While Ansible can provision some cloud resources, its core strength lies in configuring and managing software on existing systems rather than comprehensive infrastructure provisioning.\n\nAnsible provides configuration management and limited infrastructure provisioning through its agentless architecture and simple YAML-based playbooks.\n\nPulumi Integration: Rather than competing with Ansible, Pulumi complements it perfectly. Use Pulumi for infrastructure provisioning and Ansible for configuration management. Pulumi’s Command provider can execute Ansible playbooks as part of your infrastructure deployment, and many Pulumi customers use both tools together for comprehensive infrastructure automation. [See example: Deploy WordPress to AWS using Pulumi and Ansible](https://www.pulumi.com/blog/deploy-wordpress-aws-pulumi-ansible/).\n\nKey Features:\n\n- **Agentless architecture**: No software installation required on target systems\n- **YAML playbooks**: Human-readable automation definitions\n- **Idempotent operations**: Safe to run multiple times\n- **Large module library**: Extensive built-in functionality for various systems\n- **Push-based execution**: Centralized control and execution\n\nCode Example:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"nn\">---</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\">- <span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">Provision AWS infrastructure</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">hosts</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">localhost</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">tasks</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">Create VPC</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">amazon.aws.ec2_vpc_net</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">ansible-vpc</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">cidr_block</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">10.0.0.0</span><span class=\"l\">/16</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">region</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">us-east-1</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">tags</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">Environment</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">production</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">register</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">vpc</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">Create subnet</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">amazon.aws.ec2_vpc_subnet</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">vpc_id</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"{{ vpc.vpc.id }}\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">cidr</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">10.0.1.0</span><span class=\"l\">/24</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">region</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">us-east-1</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">tags</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">Name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">ansible-subnet</span><span class=\"w\">\n</span></span></span>\n```\n\n### Chef\n\nLicense: Apache 2.0  \nBest For: Complex configuration management scenarios requiring programmable logic\n\nChef is a configuration management tool, not an Infrastructure as Code tool. Chef focuses on configuring and maintaining software, services, and system settings on existing infrastructure rather than provisioning cloud resources.\n\nChef provides configuration management and system automation using Ruby-based recipes and cookbooks, offering powerful programmability for complex configuration scenarios.\n\nKey Features:\n\n- **Ruby DSL**: Full programming language for configuration logic\n- **Agent-based architecture**: Continuous compliance and drift detection\n- **Cookbook ecosystem**: Reusable configuration patterns and community recipes\n- **Test Kitchen**: Infrastructure testing and validation framework\n- **Enterprise features**: Advanced reporting and compliance capabilities\n\nCode Example:\n\n```ruby\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\"># cookbook/recipes/default.rb</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">package</span> <span class=\"s1\">'nginx'</span> <span class=\"k\">do</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">action</span> <span class=\"ss\">:install</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">end</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">service</span> <span class=\"s1\">'nginx'</span> <span class=\"k\">do</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">action</span> <span class=\"o\">[</span><span class=\"ss\">:enable</span><span class=\"p\">,</span> <span class=\"ss\">:start</span><span class=\"o\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">end</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">template</span> <span class=\"s1\">'/etc/nginx/sites-available/default'</span> <span class=\"k\">do</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">source</span> <span class=\"s1\">'default.erb'</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">owner</span> <span class=\"s1\">'root'</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">group</span> <span class=\"s1\">'root'</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">mode</span> <span class=\"s1\">'0644'</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">notifies</span> <span class=\"ss\">:restart</span><span class=\"p\">,</span> <span class=\"s1\">'service[nginx]'</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">end</span>\n</span></span>\n```\n\n### Puppet\n\nLicense: Apache 2.0  \nBest For: Enterprise environments requiring strong governance and compliance\n\nPuppet is a configuration management tool, not an Infrastructure as Code tool. Puppet specializes in maintaining desired configuration state on existing systems and ensuring compliance, rather than provisioning cloud infrastructure.\n\nPuppet offers enterprise-grade configuration management with a focus on compliance, governance, and declarative system state management.\n\nKey Features:\n\n- **Declarative language**: Puppet DSL for describing desired system state\n- **Compliance reporting**: Built-in governance and audit capabilities\n- **Forge marketplace**: Community modules and enterprise content\n- **Enterprise support**: Professional services and enterprise features\n- **Continuous enforcement**: Ongoing configuration compliance monitoring\n\nCode Example:\n\n```puppet\n<span class=\"line\"><span class=\"cl\"><span class=\"c\"># manifests/webserver.pp</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">class</span> <span class=\"na\">webserver</span> <span class=\"p\">{</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">package</span> <span class=\"p\">{</span> <span class=\"s\">'nginx'</span><span class=\"p\">:</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"na\">ensure</span> <span class=\"o\">=></span> <span class=\"k\">installed</span><span class=\"p\">,</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">service</span> <span class=\"p\">{</span> <span class=\"s\">'nginx'</span><span class=\"p\">:</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"na\">ensure</span> <span class=\"o\">=></span> <span class=\"k\">running</span><span class=\"p\">,</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"na\">enable</span> <span class=\"o\">=></span> <span class=\"k\">true</span><span class=\"p\">,</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"na\">require</span> <span class=\"o\">=></span> <span class=\"k\">Package</span><span class=\"p\">[</span><span class=\"s\">'nginx'</span><span class=\"p\">],</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">file</span> <span class=\"p\">{</span> <span class=\"s\">'/var/www/html/index.html'</span><span class=\"p\">:</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"na\">ensure</span> <span class=\"o\">=></span> <span class=\"k\">file</span><span class=\"p\">,</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"na\">content</span> <span class=\"o\">=></span> <span class=\"s\">'<h1>Hello from Puppet!</h1>'</span><span class=\"p\">,</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"na\">owner</span> <span class=\"o\">=></span> <span class=\"s\">'www-data'</span><span class=\"p\">,</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"na\">group</span> <span class=\"o\">=></span> <span class=\"s\">'www-data'</span><span class=\"p\">,</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"na\">mode</span> <span class=\"o\">=></span> <span class=\"s\">'0644'</span><span class=\"p\">,</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span><span class=\"err\">\n</span></span></span>\n```\n\n### Salt\n\nLicense: Apache 2.0  \nBest For: Python-oriented teams requiring high-performance configuration management\n\nSalt is primarily a configuration management and remote execution tool, not a pure Infrastructure as Code tool. While Salt can manage some infrastructure components, its primary focus is on configuring systems and executing commands across large infrastructures.\n\nSalt provides fast, scalable configuration management and remote execution using Python, designed for high-performance system automation at scale.\n\nKey Features:\n\n- **Python-based**: Leverage the Python ecosystem and libraries\n- **High performance**: Fast execution across large infrastructures\n- **Event-driven automation**: Reactive automation and orchestration\n- **Pillar data system**: Secure, hierarchical data management\n- **Flexible communication**: Support for various communication patterns\n\nCode Example:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"c\"># /srv/salt/webserver.sls</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">nginx</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">pkg.installed</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"p\">[]</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">service.running</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">enable</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kc\">True</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">require</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">pkg</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">nginx</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">/var/www/html/index.html</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">file.managed</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">source</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">salt://files/index.html</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">user</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">www-data</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">group</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">www-data</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">mode</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">644</span><span class=\"w\">\n</span></span></span>\n```\n\n## Infrastructure as Code Tools\n\n### Kubernetes - Container Orchestration Platform\n\nLicense: Apache 2.0  \nBest For: Container-native infrastructure and application management\n\nWhile primarily a [container orchestration](https://www.pulumi.com/topics/containers/) platform, Kubernetes itself serves as an infrastructure as code tool through its declarative YAML manifests and API-driven resource management.\n\nKey Features:\n\n- **Declarative configuration**: YAML-based resource definitions\n- **API-driven**: RESTful API for all resource operations\n- **Self-healing**: Automatic recovery and reconciliation\n- **Extensible**: Custom resources and controllers\n- **GitOps compatible**: Works seamlessly with Git-based workflows\n\nCode Example:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"nt\">apiVersion</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">apps/v1</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">kind</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">Deployment</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">metadata</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">nginx-deployment</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">spec</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">replicas</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">3</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">selector</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">matchLabels</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">app</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">nginx</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">template</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">metadata</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">labels</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">app</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">nginx</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">spec</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">containers</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">nginx</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">image</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">nginx:1.21</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ports</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">containerPort</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">80</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nn\">---</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">apiVersion</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">v1</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">kind</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">Service</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">metadata</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">nginx-service</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">spec</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">selector</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">app</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">nginx</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ports</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">port</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">80</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">targetPort</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">80</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">LoadBalancer</span><span class=\"w\">\n</span></span></span>\n```\n\n## Infrastructure as Code Security and Compliance Tools\n\nWhile the tools above focus on provisioning and managing infrastructure, a complete IaC ecosystem includes security scanning and compliance tools. These tools complement your primary IaC tool by providing security analysis, policy enforcement, and compliance checking:\n\n### Security Scanning Tools\n\nSnyk - License: Proprietary  \nLeading developer security platform that includes comprehensive infrastructure as code scanning alongside container and application security. Provides real-time vulnerability detection, compliance checking, and automated remediation guidance. Integrates with popular development tools and CI/CD pipelines with extensive enterprise adoption.\n\nWiz - License: Proprietary  \nComprehensive cloud security platform that includes infrastructure as code scanning capabilities. Provides vulnerability management, compliance monitoring, and security posture assessment across cloud environments. Offers integration with development workflows and supports multiple IaC formats with strong enterprise presence.\n\nCheckov - License: Apache 2.0  \nPopular open-source static analysis tool for infrastructure as code that scans cloud infrastructure configurations for security and compliance issues. Supports Terraform, CloudFormation, Kubernetes, Helm, ARM templates, and more. Integrates with CI/CD pipelines and provides over 1000+ built-in policies covering CIS benchmarks, PCI DSS, and GDPR compliance.\n\n### Linting and Validation Tools\n\nTFLint - License: MPL 2.0  \nTerraform linter focused on possible errors, best practices, and security issues in Terraform configurations. Provides pluggable rule sets for cloud providers (AWS, Azure, GCP) and helps enforce coding standards, detect deprecated syntax, and prevent common configuration errors.\n\nThese security tools integrate into CI/CD pipelines alongside your chosen IaC tool to provide comprehensive security coverage throughout the infrastructure lifecycle.\n\n### Pulumi ESC\n\nLicense: Apache 2.0 (Open Source) / Proprietary (SaaS)  \nBest For: Teams needing centralized configuration and secrets management across environments and tools\n\nPulumi ESC (Environments, Secrets, and Configuration) is a comprehensive platform for managing configuration data, secrets, and environment variables across your entire infrastructure and application stack. ESC provides a single source of truth for configuration that works with any infrastructure tool, not just Pulumi.\n\nKey Features:\n\n- **Universal configuration management**: Works with any infrastructure tool, CI/CD system, or application\n- **Hierarchical environments**: Compose configuration from multiple sources with inheritance and overrides\n- **Dynamic secrets**: Integration with cloud providers for short-lived credentials and just-in-time access\n- **Policy-based access control**: Fine-grained permissions and audit logging for configuration access\n- **Multiple consumption methods**: CLI, REST API, SDK integration, and direct cloud provider integration\n- **GitOps workflows**: Version control integration with pull request-based configuration management\n\nCode Example:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"c\"># Production environment configuration</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">values</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">app</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">myapp</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">version</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${fn.fromJSON(aws.ecr.getAuthorizationToken).password}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">database</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">connectionString</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${pulumi.database.connectionString}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">imports</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"l\">shared/common</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"l\">aws/production</span><span class=\"w\">\n</span></span></span>\n```\n\n### Pulumi Insights\n\nLicense: Proprietary (SaaS)  \nBest For: Organizations needing comprehensive cloud resource visibility, search, and compliance monitoring\n\nPulumi Insights provides cloud resource search, analytics, and compliance capabilities across your entire multi-cloud infrastructure, regardless of how resources were provisioned. It offers a unified view of your cloud resources with powerful search, cost analysis, and policy enforcement.\n\nKey Features:\n\n- **Universal cloud inventory**: Discover and catalog resources across AWS, Azure, Google Cloud, and Kubernetes\n- **Advanced search and analytics**: Query resources by tags, properties, relationships, and compliance status\n- **Cost optimization insights**: Resource cost analysis, trends, and optimization recommendations\n- **Compliance monitoring**: Continuous policy evaluation and drift detection across all cloud resources\n- **Resource relationships**: Understand dependencies and relationships between cloud resources\n- **Integration with any IaC tool**: Works with resources created by Pulumi, Terraform, CloudFormation, or manually\n\nUse Cases:\n\n- **Cloud governance**: Ensure compliance with organizational policies and industry standards\n- **Cost management**: Identify unused resources and optimization opportunities\n- **Security auditing**: Track resource configurations and detect security misconfigurations\n- **Multi-cloud visibility**: Single pane of glass for resources across different cloud providers\n\n## Infrastructure Automation and Management Platforms\n\nWhile the above tools focus on defining and provisioning infrastructure, several platforms provide automation, orchestration, and management capabilities that work with Infrastructure as Code tools. Important: These are not IaC tools themselves, but rather automation platforms that rely on underlying IaC tools.\n\n### IaC Automation Platforms\n\nPulumi Cloud Best For: Teams using Pulumi IaC who need enterprise-grade collaboration, security, and governance\n\nPulumi Cloud is the smartest and easiest way to automate, secure, and manage everything you run in the cloud. It serves as the managed service companion to Pulumi’s open-source infrastructure as code tool, providing enterprise capabilities that extend beyond basic IaC functionality.\n\nThe relationship between Pulumi IaC and Pulumi Cloud follows a Git/GitHub model: Pulumi IaC is like Git (fully open source and functional on its own), while Pulumi Cloud is like GitHub (a managed service that makes the open-source tool much easier to use securely at scale with teams).\n\nKey Features:\n\n- **Managed state backend**: Secure, scalable state management with transactional protocols\n- **Team collaboration**: Full audit logs, RBAC, and identity provider integration\n- **Centralized secrets management**: Pulumi ESC for configuration and secrets across environments\n- **Cloud inventory and insights**: Pulumi Insights for search, compliance, and drift detection\n- **Remote deployments**: CI/CD integrations and automated workflows\n- **Policy enforcement**: Built-in security and compliance policy as code\n- **Multi-product platform**: Integrated IaC, secrets management, and cloud insights\n\nPulumi IaC works with or without Pulumi Cloud - you can use DIY backends (S3, Azure Blob, etc.) or the managed service. However, Pulumi Cloud becomes the default experience when you install the Pulumi CLI, providing instant collaboration capabilities without the overhead of building and maintaining your own infrastructure management platform.\n\nHashiCorp Cloud Platform (HCP) - License: Proprietary (SaaS)  \nBest For: Organizations standardizing on HashiCorp tools across infrastructure and security lifecycle management\n\nHashiCorp Cloud Platform (HCP) is an enterprise-grade SaaS platform that provides unified lifecycle management for infrastructure and security operations. HCP Terraform (formerly Terraform Cloud) serves as the managed service for Terraform/OpenTofu workflows, while the broader platform integrates multiple HashiCorp tools.\n\nKey Features:\n\n- **HCP Terraform management**: Workspace organization, remote execution, and state management for Terraform\n- **Infrastructure lifecycle automation**: Continuous validation, drift detection, and module lifecycle management\n- **Private VCS access**: Secure integration with private version control repositories\n- **Policy enforcement**: Sentinel policy-as-code framework for compliance and governance\n- **Integrated security services**: HCP Vault for secrets management, HCP Consul for service networking\n- **Enterprise access controls**: RBAC, SAML SSO integration, and fine-grained permissions\n- **Module and image management**: HCP Packer for automated machine/container image creation\n\nHCP focuses specifically on HashiCorp tool integration and provides a comprehensive platform for organizations that have standardized on the HashiCorp ecosystem for infrastructure and security operations.\n\nSpacelift - Spacelift is not an Infrastructure as Code tool—it’s an automation and workflow platform that relies on other IaC tools like Terraform, OpenTofu, Pulumi, CloudFormation, and Kubernetes. Spacelift provides CI/CD pipelines, policy enforcement, and collaboration features for teams using these underlying IaC tools.\n\nEnv0 - Env0 is not an Infrastructure as Code tool—it’s an automation platform that provides workflow management, governance, and collaboration features for existing IaC tools like Terraform, OpenTofu, and Terragrunt. It adds CI/CD pipelines, cost management, and policy enforcement on top of these tools.\n\nAtlantis - An open-source tool that provides GitOps-style workflows for Terraform and OpenTofu by automatically running terraform plan and apply operations via pull request automation.\n\n### Development Environment Tools\n\nVagrant - Vagrant is not an Infrastructure as Code tool—it’s a development environment management tool that creates and configures lightweight, reproducible virtual development environments. While it can provision VMs, its focus is on local development environments rather than cloud infrastructure provisioning.\n\nDocker Compose - While not an IaC tool, Docker Compose defines multi-container applications and can be used alongside IaC tools for application deployment after infrastructure provisioning.\n\nThese platforms and tools serve important roles in the infrastructure automation ecosystem but should not be confused with Infrastructure as Code tools themselves. They enhance and orchestrate the work of actual IaC tools rather than replacing them.\n\n## The Future of Infrastructure as Code\n\nThe infrastructure as code landscape is rapidly evolving toward software engineering maturity, with several transformative trends reshaping how organizations approach infrastructure:\n\nSoftware Engineering Convergence: The most significant trend is the convergence of infrastructure and software engineering practices. Organizations are moving away from limited DSLs toward full programming languages that enable testing, debugging, refactoring, and other software engineering best practices. This shift enables infrastructure teams to leverage the same tools, skills, and methodologies that have proven successful in application development.\n\nReal-Time Cloud Integration: Native cloud provider SDK integration is becoming the standard, replacing community-maintained providers that lag behind new cloud features. Organizations expect same-day access to new cloud services without waiting weeks or months for provider updates.\n\nComprehensive Testing Paradigms: Infrastructure testing is evolving beyond basic integration tests to include unit testing, property-based testing, and continuous validation. Teams are applying test-driven development principles to infrastructure, catching issues before deployment rather than discovering them in production.\n\nInternal Developer Platform Evolution: Organizations are building sophisticated Internal Developer Platforms that provide self-service infrastructure capabilities while maintaining governance and compliance. These platforms leverage infrastructure as code tools to create standardized, reusable components that accelerate development velocity.\n\nAI-Enhanced Development: Integration of AI tools to help generate, optimize, and troubleshoot infrastructure code, with particular strength in environments that use familiar programming languages where AI assistance is most mature. Emerging technologies like Model Context Protocol (MCP) and AI prompt templates are beginning to enable more sophisticated AI-infrastructure interactions.\n\nThese trends favor tools that embrace software engineering principles from the ground up, rather than attempting to retrofit programming capabilities onto template-based or DSL-limited approaches.\n\nBring software engineering to IaC\n\nDefine infrastructure across AWS, Azure, Google Cloud, and Kubernetes in the language your team already knows, with testing, IDE support, and reusable components.\n\n[Get started](https://app.pulumi.com/signup)\n\n## Migration and Adoption Strategies\n\nOrganizations don’t need to choose between maintaining existing infrastructure and adopting modern IaC approaches. Proven migration strategies enable gradual adoption while preserving operational stability.\n\n### Incremental Adoption Approaches\n\nTeam-by-Team Migration: Start with new projects or specific teams, allowing gradual skill development and process refinement. Teams can maintain existing infrastructure while building new capabilities with modern tools.\n\nProject-by-Project Transition: Migrate individual applications or services incrementally, enabling teams to learn and optimize approaches before expanding scope.\n\nHybrid Operations: Use state integration and import tools to reference existing infrastructure while building new components with modern IaC tools.\n\n### Proven Migration Timelines\n\nReal-world migrations demonstrate that adoption can be remarkably fast with proper tooling:\n\n- **Atlassian Bitbucket**: Complete Terraform to Pulumi migration in 2 days using automated conversion tools. This rapid migration was enabled by existing Python expertise on the team, well-structured Terraform code, and straightforward infrastructure patterns that converted cleanly.\n- **Enterprise migrations**: Typical team migrations complete in weeks, not months, depending on infrastructure complexity and team preparation\n- **Learning curve**: Teams with programming experience adapt to language-based IaC approaches within days, while those new to programming may require additional training time\n\n### Migration Tools and Resources\n\nAutomated Conversion: Tools like tf2pulumi and pulumi convert automatically translate existing infrastructure definitions, preserving logic and structure while upgrading to modern approaches.\n\nState Integration: Import existing cloud resources and reference Terraform state during transition periods, enabling zero-downtime migrations.\n\nTraining and Support: Comprehensive documentation, tutorials, and community resources accelerate team onboarding and reduce migration risks.\n\n### Best Practices for Successful Adoption\n\n1. **Start with pilot projects** to build confidence and establish patterns\n2. **Leverage automated migration tools** to reduce manual conversion effort\n3. **Maintain state integration** during transition periods for operational continuity\n4. **Invest in team training** to maximize productivity gains from modern approaches\n5. **Establish testing practices** early to realize reliability and quality benefits\n\nThe key insight: migration to modern IaC approaches is a technical upgrade, not a complete rebuild. Organizations can preserve existing investments while gaining access to superior tooling and practices.\n\n## Frequently Asked Questions\n\n### Which IaC tool should I choose for AWS?\n\nFor teams wanting programming languages: Pulumi IaC and AWS CDK both offer excellent developer experiences with general-purpose programming languages. Pulumi provides multi-cloud flexibility, while CDK offers deep AWS-native integration.\n\nFor AWS-only deployments: CloudFormation provides the deepest native AWS service support and direct integration with AWS services.\n\nFor Terraform ecosystem users: OpenTofu provides open-source licensing with Terraform compatibility, while Terraform offers extensive community resources and established workflows.\n\n### Is Terraform still worth learning in 2026?\n\nTerraform remains a valuable skill and viable choice for many scenarios, though teams should consider their specific needs and long-term goals:\n\nWhen Terraform Makes Sense:\n\n- Teams building on existing Terraform infrastructure and expertise\n- Organizations with successful Terraform workflows and established processes\n- Projects that benefit from Terraform’s extensive module ecosystem\n- Teams comfortable with HCL and template-based approaches\n\nWhen to Consider Alternatives:\n\n- Teams wanting comprehensive testing capabilities (unit, property, integration testing)\n- Organizations requiring full IDE support with debugging and refactoring\n- Projects needing complex programmatic logic or extensive code reuse\n- Teams with strong programming backgrounds who prefer familiar languages\n\nKey Considerations:\n\n- **Licensing**: Terraform’s Business Source License may create restrictions for some commercial scenarios\n- **Ecosystem**: Terraform has the largest provider ecosystem and community resources\n- **Learning investment**: Consider whether learning HCL aligns with your team’s skill development goals\n- **Migration options**: Tools exist for migrating between different IaC platforms when needs change\n\nThe choice often depends on team background, infrastructure complexity, and development workflow preferences rather than one approach being universally superior.\n\n### How do I get started with infrastructure as code?\n\nChoose Based on Your Team Profile:\n\n1. **Programming-oriented teams**: Consider Pulumi IaC for familiar languages with full IDE support, testing frameworks, and multi-cloud flexibility\n2. **Template-oriented teams**: OpenTofu or Terraform offer extensive community resources, established workflows, and broad ecosystem support\n3. **Cloud-specific teams**: Native tools (AWS CDK/CloudFormation, Azure ARM/Bicep, Google Cloud Infrastructure Manager) provide deep platform integration\n4. **Kubernetes-focused teams**: Consider Crossplane or Pulumi Kubernetes Operator for container-native approaches\n\nGetting Started Steps:\n\n1. **Start small**: Begin with simple projects like deploying a single application or basic infrastructure\n2. **Use examples**: Leverage existing templates, tutorials, and community examples to accelerate learning\n3. **Plan for growth**: Consider how your tool choice will scale with team size and infrastructure complexity\n4. **Experiment**: Most tools offer free tiers or trials - try multiple approaches with simple projects to find what fits your team best\n\n### What’s the difference between configuration management and infrastructure provisioning?\n\n- Infrastructure provisioning (Pulumi IaC, Terraform, CloudFormation) creates and manages cloud resources like VMs, networks, and databases\n- Configuration management (Ansible, Chef, Puppet) configures and maintains software on existing systems\n- Many modern tools do both, with Pulumi IaC and Ansible offering comprehensive capabilities across both domains\n\n### Can I use multiple IaC tools together?\n\nAbsolutely! Many organizations use complementary tools for different aspects of infrastructure management:\n\nPulumi + Configuration Management:\n\n- Pulumi + Ansible: Infrastructure provisioning + server configuration ([example](https://www.pulumi.com/blog/deploy-wordpress-aws-pulumi-ansible/))\n- Pulumi + Chef/Puppet: [Cloud resources](https://www.pulumi.com/docs/get-started/) + complex configuration management\n\nPulumi + Native Cloud Tools:\n\n- Reference existing CloudFormation/ARM deployments from Pulumi programs\n- Import CloudFormation/ARM resources into Pulumi for gradual migration\n- Use Pulumi alongside CDK for different parts of AWS infrastructure\n\nPulumi + Kubernetes:\n\n- [Pulumi Kubernetes Operator (PKO)](https://www.pulumi.com/docs/using-pulumi/continuous-delivery/pulumi-kubernetes-operator/) for GitOps workflows\n- Pulumi + Crossplane for layered infrastructure management\n- Pulumi + security scanners like Checkov or Terrascan for compliance\n\nTerraform + Pulumi Coexistence:\n\n- Use Terraform for base infrastructure and Pulumi for Internal Developer Platforms (IDPs)\n- Reference existing Terraform state from Pulumi programs during gradual migration\n- Manage different infrastructure layers with different tools based on team expertise\n\nPulumi IaC is designed for heterogeneous environments where multiple tools may be in use. For example, you can manage existing Terraform or CloudFormation resources with Pulumi, either using both tools in tandem or for temporary management while migrating to native Pulumi IaC code. Other Pulumi tools, like Pulumi IDP, also enable you to manage IaC self-service workflows like other tools on this list. See what’s possible when [migrating to Pulumi](https://www.pulumi.com/docs/iac/adopting-pulumi/migrating-to-pulumi/).\n\n### Which tool has the best learning resources?\n\n- Pulumi: Excellent documentation with examples in multiple languages, comprehensive tutorials\n- Terraform/OpenTofu: Extensive community content, many courses and books available\n- AWS CDK: Outstanding official documentation with workshops and examples\n- Kubernetes: Vast ecosystem but steeper learning curve\n\n### How important is open-source licensing for IaC tools?\n\nVery important for long-term strategy:\n\n- Apache 2.0 (Pulumi, CDK) offers maximum flexibility\n- MPL 2.0 (OpenTofu) ensures open-source availability\n- BSL (Terraform) restricts commercial competitors\n- Proprietary (CloudFormation, ARM) ties you to specific vendors\n\nChoose open-source tools like Pulumi or OpenTofu to avoid vendor lock-in.\n\n### What about security and compliance?\n\nAll major IaC tools support security best practices:\n\n- Built-in policy engines: Pulumi CrossGuard, AWS Config, Azure Policy\n- Third-party scanners: Checkov, KICS, Terrascan work with all tools\n- Compliance frameworks: Most tools support SOC 2, PCI DSS, CIS benchmarks\n- Secret management: Integration with Vault, AWS Secrets Manager, Azure Key Vault\n\nPulumi offers the most comprehensive built-in security features with CrossGuard policy as code.\n\n### Can I migrate from one IaC tool to another?\n\nYes, and many organizations are successfully migrating to overcome limitations in their current tools. Migration tools and proven approaches exist:\n\nProven Migration Success Stories:\n\n- **Atlassian Bitbucket**: Completed migration from Terraform to Pulumi’s Python-based IaC in just 2 days using migration tools\n- **Starburst**: Achieved 112x faster deployments after incrementally migrating from Terraform\n- **Multiple organizations**: Report 80-90% deployment time reductions after moving to programming language-based approaches\n\nAvailable Migration Tools:\n\n- **Pulumi**: Offers [`pulumi convert`](https://www.pulumi.com/docs/using-pulumi/adopting-pulumi/migrating-to-pulumi/#conversion) for importing from Terraform, ARM, and CloudFormation with state integration\n- **Terraformer**: Can import existing cloud resources into Terraform/OpenTofu\n- **CDK Migrate**: Helps move from CloudFormation to CDK\n- **Manual migration**: Always possible by recreating resources in the new tool\n\nMigration Strategies:\n\n- **Incremental adoption**: Migrate team-by-team or project-by-project without disruption\n- **State integration**: Reference existing infrastructure during transition periods\n- **Zero downtime**: Tools like Pulumi enable seamless migration without service interruption\n\nThe key is choosing tools that provide comprehensive migration support and incremental adoption paths rather than requiring “rip and replace” approaches.\n\n## Conclusion: The Evolution of Infrastructure as Code\n\nThe infrastructure as code landscape in 2026 reflects a maturing field where different approaches serve different organizational needs and team preferences. The evolution from manual processes to automated infrastructure has branched into multiple viable paths, each with distinct advantages.\n\nThe Spectrum of Approaches:\n\nTemplate-based tools like Terraform and OpenTofu continue to serve teams effectively, particularly those comfortable with declarative configuration and established HCL workflows. Cloud-native solutions provide deep platform integration for single-cloud strategies. Programming language-based approaches offer familiar development experiences for teams seeking comprehensive testing, IDE integration, and code reusability.\n\nEvidence of Success Across Approaches:\n\nOrganizations achieve significant improvements regardless of their chosen path—the key is selecting tools that align with team expertise and infrastructure requirements. Understanding [what DevOps is](https://www.pulumi.com/what-is/what-is-devops/) and [platform engineering concepts](https://www.pulumi.com/what-is/what-is-platform-engineering/) helps inform these decisions. Success stories span the entire ecosystem, from Terraform’s widespread enterprise adoption to programming language approaches enabling dramatic productivity gains at companies like Unity, Snowflake, and Starburst.\n\nChoosing Your Path:\n\nThe most successful organizations focus on key decision criteria:\n\n- **Team background**: Match tools to existing skills and preferred development approaches\n- **Infrastructure complexity**: Consider testing, abstraction, and maintainability needs\n- **Organizational constraints**: Factor in licensing, governance, and compliance requirements\n- **Growth trajectory**: Plan for how needs may evolve with scale and team expansion\n- **Integration requirements**: Ensure compatibility with existing workflows and toolchains\n\nThe Future of Infrastructure:\n\nThe industry continues evolving toward treating infrastructure as software, but this transformation takes many forms. Organizations exploring [serverless architectures](https://www.pulumi.com/serverless/) and container strategies particularly benefit from programmable infrastructure approaches. Whether through enhanced DSLs, visual design tools, programming languages, or hybrid approaches, the goal remains consistent: enabling teams to manage infrastructure with the same reliability, collaboration, and velocity they expect from modern software development.\n\nFor teams ready to embrace programming language-based infrastructure as code, [get started with Pulumi for free](https://www.pulumi.com/docs/install/) to experience how familiar languages and software engineering practices can transform infrastructure management with comprehensive testing, powerful abstractions, and seamless multi-cloud support. Want to see where infrastructure is headed next? [Explore Pulumi Neo](https://www.pulumi.com/product/neo/), the AI agent that proposes changes, runs previews, and opens PRs alongside your team.","publishedAt":"2026-07-05T00:00:00.000Z","fetchedAt":"2026-07-16T21:20:02.353Z","url":"https://www.pulumi.com/blog/infrastructure-as-code-tools/","media":[{"type":"image","url":"https://www.pulumi.com/images/generated/blog/infrastructure-as-code-tools/index.png","r2Key":"releases/d5455ea11e6772d1c1ed5a2ea9633b1b572554543345fbf822726de982acee11.png","r2Url":"https://media.releases.sh/releases/d5455ea11e6772d1c1ed5a2ea9633b1b572554543345fbf822726de982acee11.png"}],"coverageCount":0},{"id":"rel_4jEIZIyc_QcKbYquNX7nV","version":null,"type":"feature","title":"How to Test Infrastructure as Code","summary":"![](https://www.pulumi.com/blog/how-to-test-infrastructure-as-code/meta.png)\n\nIaC testing means validating your infrastructure code the same way you t...","titleGenerated":null,"titleShort":null,"breaking":"unknown","importance":null,"content":"![](https://www.pulumi.com/blog/how-to-test-infrastructure-as-code/meta.png)\n\nIaC testing means validating your infrastructure code the same way you test application software—unit tests with mocked cloud providers that run in milliseconds, integration tests that deploy and inspect real resources, and policy checks that enforce compliance rules on every preview and deploy. Together, these layers catch misconfigurations before they reach production.\n\nUntested infrastructure code is a liability. A missing tag in a security group rule, an S3 bucket with public read access, a misconfigured IAM policy—any of these can slip through code review and land in production. Infrastructure bugs are often harder to debug than application bugs, because the feedback loop is slow (deploy, observe, destroy) and the blast radius is large (an outage, a security incident, a surprise AWS bill).\n\nThe good news: Pulumi is built on general-purpose programming languages, which means you can test infrastructure code with the exact same tools and frameworks you already use to test application code. No new language to learn. No separate toolchain. Just pytest, Mocha, or `go test`—pointing at your infrastructure program.\n\nThis guide walks through all three testing layers with complete, runnable examples.\n\n## Why should you test infrastructure as code?\n\nThe case for testing application code is well-established. The case for testing IaC is just as strong—arguably stronger, because the consequences of an undetected bug are more severe.\n\n**Catch misconfigurations before they deploy.** A unit test that checks whether a security group allows SSH from 0.0.0.0/0 will catch that misconfiguration in milliseconds, before any cloud API call is made. The same test in CI catches it before the code is merged.\n\n**Prevent drift and outages.** Integration tests that deploy to a staging environment and validate runtime behavior—does the HTTP endpoint return 200? does the database accept connections?—give you confidence that an infrastructure change doesn’t silently break a dependent service.\n\n**Refactor safely.** Well-tested infrastructure code can be restructured, modularized, and upgraded without fear. When all your properties, tags, and security rules are asserted in tests, you know immediately if a refactor breaks a constraint.\n\n**Enforce security and compliance automatically.** Policy as code lets you encode your organization’s security rules as executable tests. Mandatory policies block non-compliant deployments entirely; advisory policies surface warnings for human review. These rules run on every `pulumi preview` and `pulumi up`—not just at audit time.\n\n**Ship infrastructure at software speed.** Teams that test IaC ship changes faster, with less fear. The CI pipeline becomes a confidence engine rather than a deployment gate.\n\nThe annual [DORA State of DevOps research](https://cloud.google.com/devops/state-of-devops) is consistently associated with a pattern among elite-performing engineering teams—those that deploy frequently and recover quickly from failures: comprehensive automated testing across the stack. Infrastructure is no longer exempt from that standard.\n\n## What types of IaC tests are there?\n\nIaC testing mirrors the classic software test pyramid: a wide base of fast, cheap unit tests, a smaller layer of integration tests against real infrastructure, and policy checks that enforce rules throughout.\n\nLayer\n\nWhat it tests\n\nSpeed\n\nCost\n\nCloud credentials?\n\n**Unit tests**\n\nResource properties, tags, security rules, naming conventions\n\nMilliseconds\n\nFree\n\nNo\n\n**Integration tests**\n\nFull stack lifecycle, runtime behavior, cross-resource dependencies\n\nMinutes\n\nReal cloud spend\n\nYes\n\n**Policy as code**\n\nCompliance rules, security baselines, organizational standards\n\nMilliseconds\n\nFree\n\nNo (runs at preview time)\n\n```\ngraph TD\nA[\"🔒 Policy as Code<br/>Compliance & guardrails<br/>Runs at preview time<br/>Milliseconds · Free\"] --> B\nB[\"🧪 Integration Tests<br/>Real cloud deployments<br/>Full lifecycle validation<br/>Minutes · Cloud spend\"] --> C\nC[\"⚡ Unit Tests<br/>Mocked providers<br/>Property assertions<br/>Milliseconds · Free\"]\nstyle A fill:#8A3FFC,color:#fff,stroke:#6B2FD4\nstyle B fill:#0EA5E9,color:#fff,stroke:#0284C7\nstyle C fill:#10B981,color:#fff,stroke:#059669\n```\n\nStart with unit tests—they’re free, fast, and catch the most common mistakes. Add integration tests for critical paths (the deployment pipeline, data infrastructure, security-sensitive resources). Layer in policy as code for compliance requirements that must never regress.\n\n## How do you write unit tests for Pulumi?\n\nPulumi unit tests replace the communication channel between the Pulumi program and the cloud provider with **mocks**. When the program registers a resource (e.g., `aws.ec2.SecurityGroup(...)`), the mock intercepts the call, returns dummy state, and never makes a real cloud API call. Tests run in the same process, in the same language, using the same test runner you already use.\n\n### Setting up mocks\n\n**The import-order rule:** You must set up mocks *before* importing your Pulumi program. If you import the program first, the Pulumi runtime initializes without mocks and will attempt real cloud calls.\n\n```python\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\"># test_infra.py</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">unittest</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">pulumi</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">class</span> <span class=\"nc\">MyMocks</span><span class=\"p\">(</span><span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">runtime</span><span class=\"o\">.</span><span class=\"n\">Mocks</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">def</span> <span class=\"nf\">new_resource</span><span class=\"p\">(</span><span class=\"bp\">self</span><span class=\"p\">,</span> <span class=\"n\">args</span><span class=\"p\">:</span> <span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">runtime</span><span class=\"o\">.</span><span class=\"n\">MockResourceArgs</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\"># Return [id, state]. State keys must be camelCase.</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">return</span> <span class=\"p\">[</span><span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">name</span> <span class=\"o\">+</span> <span class=\"s2\">\"_id\"</span><span class=\"p\">,</span> <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">inputs</span><span class=\"p\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">def</span> <span class=\"nf\">call</span><span class=\"p\">(</span><span class=\"bp\">self</span><span class=\"p\">,</span> <span class=\"n\">args</span><span class=\"p\">:</span> <span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">runtime</span><span class=\"o\">.</span><span class=\"n\">MockCallArgs</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">return</span> <span class=\"p\">{}</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Set mocks BEFORE importing the program</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">runtime</span><span class=\"o\">.</span><span class=\"n\">set_mocks</span><span class=\"p\">(</span><span class=\"n\">MyMocks</span><span class=\"p\">())</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">infra</span> <span class=\"c1\"># import program AFTER setting mocks</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">class</span> <span class=\"nc\">TestInfra</span><span class=\"p\">(</span><span class=\"n\">unittest</span><span class=\"o\">.</span><span class=\"n\">TestCase</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nd\">@pulumi.runtime.test</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">def</span> <span class=\"nf\">test_no_public_ssh</span><span class=\"p\">(</span><span class=\"bp\">self</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"\"\"Security group must not expose SSH to the public internet.\"\"\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">def</span> <span class=\"nf\">check_security_group</span><span class=\"p\">(</span><span class=\"n\">args</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">ingress_rules</span> <span class=\"o\">=</span> <span class=\"n\">args</span><span class=\"p\">[</span><span class=\"mi\">0</span><span class=\"p\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">for</span> <span class=\"n\">rule</span> <span class=\"ow\">in</span> <span class=\"p\">(</span><span class=\"n\">ingress_rules</span> <span class=\"ow\">or</span> <span class=\"p\">[]):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">for</span> <span class=\"n\">cidr</span> <span class=\"ow\">in</span> <span class=\"p\">(</span><span class=\"n\">rule</span><span class=\"o\">.</span><span class=\"n\">get</span><span class=\"p\">(</span><span class=\"s2\">\"cidrBlocks\"</span><span class=\"p\">)</span> <span class=\"ow\">or</span> <span class=\"p\">[]):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">assertNotEqual</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">cidr</span><span class=\"p\">,</span> <span class=\"s2\">\"0.0.0.0/0\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"Security group must not expose port 22 to the internet.\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">return</span> <span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">Output</span><span class=\"o\">.</span><span class=\"n\">all</span><span class=\"p\">(</span><span class=\"n\">infra</span><span class=\"o\">.</span><span class=\"n\">group</span><span class=\"o\">.</span><span class=\"n\">ingress</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">apply</span><span class=\"p\">(</span><span class=\"n\">check_security_group</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nd\">@pulumi.runtime.test</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">def</span> <span class=\"nf\">test_server_has_tags</span><span class=\"p\">(</span><span class=\"bp\">self</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"\"\"EC2 instance must have required tags.\"\"\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">def</span> <span class=\"nf\">check_tags</span><span class=\"p\">(</span><span class=\"n\">args</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">tags</span> <span class=\"o\">=</span> <span class=\"n\">args</span><span class=\"p\">[</span><span class=\"mi\">0</span><span class=\"p\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">assertIn</span><span class=\"p\">(</span><span class=\"s2\">\"Environment\"</span><span class=\"p\">,</span> <span class=\"n\">tags</span><span class=\"p\">,</span> <span class=\"s2\">\"Missing 'Environment' tag\"</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"bp\">self</span><span class=\"o\">.</span><span class=\"n\">assertIn</span><span class=\"p\">(</span><span class=\"s2\">\"Name\"</span><span class=\"p\">,</span> <span class=\"n\">tags</span><span class=\"p\">,</span> <span class=\"s2\">\"Missing 'Name' tag\"</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">return</span> <span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">Output</span><span class=\"o\">.</span><span class=\"n\">all</span><span class=\"p\">(</span><span class=\"n\">infra</span><span class=\"o\">.</span><span class=\"n\">server</span><span class=\"o\">.</span><span class=\"n\">tags</span><span class=\"p\">)</span><span class=\"o\">.</span><span class=\"n\">apply</span><span class=\"p\">(</span><span class=\"n\">check_tags</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">if</span> <span class=\"vm\">__name__</span> <span class=\"o\">==</span> <span class=\"s2\">\"__main__\"</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">unittest</span><span class=\"o\">.</span><span class=\"n\">main</span><span class=\"p\">()</span>\n</span></span>\n```\n\nRun with: `python -m unittest discover -v`\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// infra.test.ts\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">pulumi</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/pulumi\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"p\">{</span> <span class=\"nx\">expect</span> <span class=\"p\">}</span> <span class=\"kr\">from</span> <span class=\"s2\">\"chai\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Set mocks BEFORE dynamic import of the program\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">runtime</span><span class=\"p\">.</span><span class=\"nx\">setMocks</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">newResource</span><span class=\"o\">:</span> <span class=\"p\">(</span><span class=\"nx\">args</span>: <span class=\"kt\">pulumi.runtime.MockResourceArgs</span><span class=\"p\">)</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">id</span>: <span class=\"kt\">string</span><span class=\"p\">;</span> <span class=\"nx\">state</span>: <span class=\"kt\">any</span> <span class=\"p\">}</span> <span class=\"o\">=></span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// State keys must be camelCase\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">return</span> <span class=\"p\">{</span> <span class=\"nx\">id</span><span class=\"o\">:</span> <span class=\"sb\">`</span><span class=\"si\">${</span><span class=\"nx\">args</span><span class=\"p\">.</span><span class=\"nx\">name</span><span class=\"si\">}</span><span class=\"sb\">-id`</span><span class=\"p\">,</span> <span class=\"nx\">state</span>: <span class=\"kt\">args.inputs</span> <span class=\"p\">};</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">call</span><span class=\"o\">:</span> <span class=\"p\">(</span><span class=\"nx\">args</span>: <span class=\"kt\">pulumi.runtime.MockCallArgs</span><span class=\"p\">)</span> <span class=\"o\">=></span> <span class=\"p\">({</span> <span class=\"p\">...</span><span class=\"nx\">args</span><span class=\"p\">.</span><span class=\"nx\">inputs</span> <span class=\"p\">}),</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"my-project\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"test\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kc\">false</span> <span class=\"c1\">// preview = false\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"nx\">describe</span><span class=\"p\">(</span><span class=\"s2\">\"Infrastructure\"</span><span class=\"p\">,</span> <span class=\"kd\">function</span> <span class=\"p\">()</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kd\">let</span> <span class=\"nx\">infra</span>: <span class=\"kt\">typeof</span> <span class=\"kr\">import</span><span class=\"p\">(</span><span class=\"s2\">\"./index\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">before</span><span class=\"p\">(</span><span class=\"kr\">async</span> <span class=\"kd\">function</span> <span class=\"p\">()</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Dynamic import AFTER setting mocks\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">infra</span> <span class=\"o\">=</span> <span class=\"k\">await</span> <span class=\"kr\">import</span><span class=\"p\">(</span><span class=\"s2\">\"./index\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">it</span><span class=\"p\">(</span><span class=\"s2\">\"must not expose SSH to the internet\"</span><span class=\"p\">,</span> <span class=\"kd\">function</span> <span class=\"p\">(</span><span class=\"nx\">done</span><span class=\"p\">)</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">all</span><span class=\"p\">([</span><span class=\"nx\">infra</span><span class=\"p\">.</span><span class=\"nx\">group</span><span class=\"p\">.</span><span class=\"nx\">ingress</span><span class=\"p\">]).</span><span class=\"nx\">apply</span><span class=\"p\">(([</span><span class=\"nx\">ingress</span><span class=\"p\">])</span> <span class=\"o\">=></span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">for</span> <span class=\"p\">(</span><span class=\"kr\">const</span> <span class=\"nx\">rule</span> <span class=\"k\">of</span> <span class=\"nx\">ingress</span> <span class=\"o\">??</span> <span class=\"p\">[])</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">for</span> <span class=\"p\">(</span><span class=\"kr\">const</span> <span class=\"nx\">cidr</span> <span class=\"k\">of</span> <span class=\"nx\">rule</span><span class=\"p\">.</span><span class=\"nx\">cidrBlocks</span> <span class=\"o\">??</span> <span class=\"p\">[])</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">expect</span><span class=\"p\">(</span><span class=\"nx\">cidr</span><span class=\"p\">).</span><span class=\"nx\">to</span><span class=\"p\">.</span><span class=\"nx\">not</span><span class=\"p\">.</span><span class=\"nx\">equal</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"0.0.0.0/0\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"Security group must not expose port 22 to the internet\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">done</span><span class=\"p\">();</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">it</span><span class=\"p\">(</span><span class=\"s2\">\"must have required tags\"</span><span class=\"p\">,</span> <span class=\"kd\">function</span> <span class=\"p\">(</span><span class=\"nx\">done</span><span class=\"p\">)</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">all</span><span class=\"p\">([</span><span class=\"nx\">infra</span><span class=\"p\">.</span><span class=\"nx\">server</span><span class=\"p\">.</span><span class=\"nx\">tags</span><span class=\"p\">]).</span><span class=\"nx\">apply</span><span class=\"p\">(([</span><span class=\"nx\">tags</span><span class=\"p\">])</span> <span class=\"o\">=></span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">expect</span><span class=\"p\">(</span><span class=\"nx\">tags</span><span class=\"p\">).</span><span class=\"nx\">to</span><span class=\"p\">.</span><span class=\"nx\">include</span><span class=\"p\">.</span><span class=\"nx\">keys</span><span class=\"p\">(</span><span class=\"s2\">\"Environment\"</span><span class=\"p\">,</span> <span class=\"s2\">\"Name\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">done</span><span class=\"p\">();</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\nRun with: `mocha -r ts-node/register infra.test.ts`\n\n```go\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// infra_test.go</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">package</span><span class=\"w\"> </span><span class=\"nx\">infra_test</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"testing\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ec2\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"github.com/stretchr/testify/assert\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// MockResourceMonitor implements pulumi.ResourceMonitor for unit tests.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">func</span><span class=\"w\"> </span><span class=\"nf\">TestNoPublicSSH</span><span class=\"p\">(</span><span class=\"nx\">t</span><span class=\"w\"> </span><span class=\"o\">*</span><span class=\"nx\">testing</span><span class=\"p\">.</span><span class=\"nx\">T</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">RunErr</span><span class=\"p\">(</span><span class=\"kd\">func</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"w\"> </span><span class=\"o\">*</span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">Context</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"kt\">error</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">infra</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"nf\">NewInfra</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">assert</span><span class=\"p\">.</span><span class=\"nf\">NoError</span><span class=\"p\">(</span><span class=\"nx\">t</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">infra</span><span class=\"p\">.</span><span class=\"nx\">Group</span><span class=\"p\">.</span><span class=\"nx\">Ingress</span><span class=\"p\">.</span><span class=\"nf\">ApplyT</span><span class=\"p\">(</span><span class=\"kd\">func</span><span class=\"p\">(</span><span class=\"nx\">rules</span><span class=\"w\"> </span><span class=\"p\">[]</span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">SecurityGroupIngress</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"kt\">error</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">for</span><span class=\"w\"> </span><span class=\"nx\">_</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">rule</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"k\">range</span><span class=\"w\"> </span><span class=\"nx\">rules</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">for</span><span class=\"w\"> </span><span class=\"nx\">_</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">cidr</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"k\">range</span><span class=\"w\"> </span><span class=\"nx\">rule</span><span class=\"p\">.</span><span class=\"nx\">CidrBlocks</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">assert</span><span class=\"p\">.</span><span class=\"nf\">NotEqual</span><span class=\"p\">(</span><span class=\"nx\">t</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"0.0.0.0/0\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">cidr</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"Security group must not expose port 22 to the internet\"</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">})</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">},</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">WithMocks</span><span class=\"p\">(</span><span class=\"s\">\"my-project\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"test\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">mocks</span><span class=\"p\">{}))</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">assert</span><span class=\"p\">.</span><span class=\"nf\">NoError</span><span class=\"p\">(</span><span class=\"nx\">t</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">type</span><span class=\"w\"> </span><span class=\"nx\">mocks</span><span class=\"w\"> </span><span class=\"kd\">struct</span><span class=\"p\">{}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">func</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"nx\">mocks</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"nf\">NewResource</span><span class=\"p\">(</span><span class=\"nx\">args</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">MockResourceArgs</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"kt\">string</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"kd\">map</span><span class=\"p\">[</span><span class=\"kt\">string</span><span class=\"p\">]</span><span class=\"kd\">interface</span><span class=\"p\">{},</span><span class=\"w\"> </span><span class=\"kt\">error</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"nx\">args</span><span class=\"p\">.</span><span class=\"nx\">Name</span><span class=\"w\"> </span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"s\">\"_id\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">args</span><span class=\"p\">.</span><span class=\"nx\">Inputs</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">func</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"nx\">mocks</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"nf\">Call</span><span class=\"p\">(</span><span class=\"nx\">args</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">MockCallArgs</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"kd\">map</span><span class=\"p\">[</span><span class=\"kt\">string</span><span class=\"p\">]</span><span class=\"kd\">interface</span><span class=\"p\">{},</span><span class=\"w\"> </span><span class=\"kt\">error</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"nx\">args</span><span class=\"p\">.</span><span class=\"nx\">Args</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span><span class=\"w\">\n</span></span></span>\n```\n\nRun with: `go test ./... -v`\n\n```csharp\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// InfraTests.cs</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">System.Collections.Generic</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">System.Linq</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">System.Threading.Tasks</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">Pulumi</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">Pulumi.Testing</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">Xunit</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">public</span> <span class=\"k\">class</span> <span class=\"nc\">InfraTests</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"na\"> [Fact]</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kd\">public</span> <span class=\"kd\">async</span> <span class=\"n\">Task</span> <span class=\"n\">NoPublicSSH</span><span class=\"p\">()</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"n\">resources</span> <span class=\"p\">=</span> <span class=\"k\">await</span> <span class=\"n\">Testing</span><span class=\"p\">.</span><span class=\"n\">RunAsync</span><span class=\"p\"><</span><span class=\"n\">MyStack</span><span class=\"p\">>();</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"n\">sg</span> <span class=\"p\">=</span> <span class=\"n\">resources</span><span class=\"p\">.</span><span class=\"n\">OfType</span><span class=\"p\"><</span><span class=\"n\">Pulumi</span><span class=\"p\">.</span><span class=\"n\">Aws</span><span class=\"p\">.</span><span class=\"n\">Ec2</span><span class=\"p\">.</span><span class=\"n\">SecurityGroup</span><span class=\"p\">>().</span><span class=\"n\">First</span><span class=\"p\">();</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"n\">ingress</span> <span class=\"p\">=</span> <span class=\"k\">await</span> <span class=\"n\">sg</span><span class=\"p\">.</span><span class=\"n\">Ingress</span><span class=\"p\">.</span><span class=\"n\">GetValueAsync</span><span class=\"p\">();</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">foreach</span> <span class=\"p\">(</span><span class=\"kt\">var</span> <span class=\"n\">rule</span> <span class=\"k\">in</span> <span class=\"n\">ingress</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">foreach</span> <span class=\"p\">(</span><span class=\"kt\">var</span> <span class=\"n\">cidr</span> <span class=\"k\">in</span> <span class=\"n\">rule</span><span class=\"p\">.</span><span class=\"n\">CidrBlocks</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">Assert</span><span class=\"p\">.</span><span class=\"n\">NotEqual</span><span class=\"p\">(</span><span class=\"s\">\"0.0.0.0/0\"</span><span class=\"p\">,</span> <span class=\"n\">cidr</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"na\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"na\"> [Fact]</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kd\">public</span> <span class=\"kd\">async</span> <span class=\"n\">Task</span> <span class=\"n\">ServerHasRequiredTags</span><span class=\"p\">()</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"n\">resources</span> <span class=\"p\">=</span> <span class=\"k\">await</span> <span class=\"n\">Testing</span><span class=\"p\">.</span><span class=\"n\">RunAsync</span><span class=\"p\"><</span><span class=\"n\">MyStack</span><span class=\"p\">>();</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"n\">server</span> <span class=\"p\">=</span> <span class=\"n\">resources</span><span class=\"p\">.</span><span class=\"n\">OfType</span><span class=\"p\"><</span><span class=\"n\">Pulumi</span><span class=\"p\">.</span><span class=\"n\">Aws</span><span class=\"p\">.</span><span class=\"n\">Ec2</span><span class=\"p\">.</span><span class=\"n\">Instance</span><span class=\"p\">>().</span><span class=\"n\">First</span><span class=\"p\">();</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"n\">tags</span> <span class=\"p\">=</span> <span class=\"k\">await</span> <span class=\"n\">server</span><span class=\"p\">.</span><span class=\"n\">Tags</span><span class=\"p\">.</span><span class=\"n\">GetValueAsync</span><span class=\"p\">();</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">Assert</span><span class=\"p\">.</span><span class=\"n\">True</span><span class=\"p\">(</span><span class=\"n\">tags</span><span class=\"p\">.</span><span class=\"n\">ContainsKey</span><span class=\"p\">(</span><span class=\"s\">\"Environment\"</span><span class=\"p\">),</span> <span class=\"s\">\"Missing 'Environment' tag\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">Assert</span><span class=\"p\">.</span><span class=\"n\">True</span><span class=\"p\">(</span><span class=\"n\">tags</span><span class=\"p\">.</span><span class=\"n\">ContainsKey</span><span class=\"p\">(</span><span class=\"s\">\"Name\"</span><span class=\"p\">),</span> <span class=\"s\">\"Missing 'Name' tag\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span>\n```\n\nRun with: `dotnet test`\n\n```java\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// InfraTest.java</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">package</span><span class=\"w\"> </span><span class=\"nn\">myproject</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.aws.ec2.Instance</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.aws.ec2.SecurityGroup</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.test.Mocks</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.test.Mocks.CallArgs</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.test.Mocks.ResourceArgs</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.test.Mocks.ResourceResult</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.test.PulumiTest</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.test.TestOptions</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">org.junit.jupiter.api.AfterEach</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">org.junit.jupiter.api.Test</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">java.util.HashMap</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">java.util.Map</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">java.util.Optional</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">java.util.concurrent.CompletableFuture</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import static</span><span class=\"w\"> </span><span class=\"nn\">org.junit.jupiter.api.Assertions.*</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">class</span> <span class=\"nc\">InfraTest</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c1\">// Implement Mocks to intercept resource creation. State keys must be camelCase.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">static</span><span class=\"w\"> </span><span class=\"kd\">class</span> <span class=\"nc\">MyMocks</span><span class=\"w\"> </span><span class=\"kd\">implements</span><span class=\"w\"> </span><span class=\"n\">Mocks</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nd\">@Override</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">public</span><span class=\"w\"> </span><span class=\"n\">CompletableFuture</span><span class=\"o\"><</span><span class=\"n\">ResourceResult</span><span class=\"o\">></span><span class=\"w\"> </span><span class=\"nf\">newResourceAsync</span><span class=\"p\">(</span><span class=\"n\">ResourceArgs</span><span class=\"w\"> </span><span class=\"n\">args</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">var</span><span class=\"w\"> </span><span class=\"n\">state</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"k\">new</span><span class=\"w\"> </span><span class=\"n\">HashMap</span><span class=\"o\"><></span><span class=\"p\">(</span><span class=\"n\">args</span><span class=\"p\">.</span><span class=\"na\">inputs</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"n\">CompletableFuture</span><span class=\"p\">.</span><span class=\"na\">completedFuture</span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">ResourceResult</span><span class=\"p\">.</span><span class=\"na\">of</span><span class=\"p\">(</span><span class=\"n\">Optional</span><span class=\"p\">.</span><span class=\"na\">of</span><span class=\"p\">(</span><span class=\"n\">args</span><span class=\"p\">.</span><span class=\"na\">name</span><span class=\"w\"> </span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"s\">\"_id\"</span><span class=\"p\">),</span><span class=\"w\"> </span><span class=\"n\">state</span><span class=\"p\">));</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nd\">@Override</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">public</span><span class=\"w\"> </span><span class=\"n\">CompletableFuture</span><span class=\"o\"><</span><span class=\"n\">Map</span><span class=\"o\"><</span><span class=\"n\">String</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">Object</span><span class=\"o\">>></span><span class=\"w\"> </span><span class=\"nf\">callAsync</span><span class=\"p\">(</span><span class=\"n\">CallArgs</span><span class=\"w\"> </span><span class=\"n\">args</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"n\">CompletableFuture</span><span class=\"p\">.</span><span class=\"na\">completedFuture</span><span class=\"p\">(</span><span class=\"n\">Map</span><span class=\"p\">.</span><span class=\"na\">of</span><span class=\"p\">());</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c1\">// PulumiTest.cleanup() must run after every test to reset the runtime state.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nd\">@AfterEach</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kt\">void</span><span class=\"w\"> </span><span class=\"nf\">cleanup</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">PulumiTest</span><span class=\"p\">.</span><span class=\"na\">cleanup</span><span class=\"p\">();</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nd\">@Test</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kt\">void</span><span class=\"w\"> </span><span class=\"nf\">serverHasRequiredTags</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">var</span><span class=\"w\"> </span><span class=\"n\">result</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">PulumiTest</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">withMocks</span><span class=\"p\">(</span><span class=\"k\">new</span><span class=\"w\"> </span><span class=\"n\">MyMocks</span><span class=\"p\">())</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">withOptions</span><span class=\"p\">(</span><span class=\"n\">TestOptions</span><span class=\"p\">.</span><span class=\"na\">builder</span><span class=\"p\">()</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">projectName</span><span class=\"p\">(</span><span class=\"s\">\"project\"</span><span class=\"p\">).</span><span class=\"na\">stackName</span><span class=\"p\">(</span><span class=\"s\">\"stack\"</span><span class=\"p\">).</span><span class=\"na\">preview</span><span class=\"p\">(</span><span class=\"kc\">false</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">build</span><span class=\"p\">())</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">runTest</span><span class=\"p\">(</span><span class=\"n\">ctx</span><span class=\"w\"> </span><span class=\"o\">-></span><span class=\"w\"> </span><span class=\"k\">new</span><span class=\"w\"> </span><span class=\"n\">Infra</span><span class=\"p\">());</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">var</span><span class=\"w\"> </span><span class=\"n\">instance</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">result</span><span class=\"p\">.</span><span class=\"na\">resources</span><span class=\"p\">().</span><span class=\"na\">stream</span><span class=\"p\">()</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">filter</span><span class=\"p\">(</span><span class=\"n\">r</span><span class=\"w\"> </span><span class=\"o\">-></span><span class=\"w\"> </span><span class=\"n\">r</span><span class=\"w\"> </span><span class=\"k\">instanceof</span><span class=\"w\"> </span><span class=\"n\">Instance</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">map</span><span class=\"p\">(</span><span class=\"n\">r</span><span class=\"w\"> </span><span class=\"o\">-></span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">Instance</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"n\">r</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">findFirst</span><span class=\"p\">()</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">orElseThrow</span><span class=\"p\">(()</span><span class=\"w\"> </span><span class=\"o\">-></span><span class=\"w\"> </span><span class=\"k\">new</span><span class=\"w\"> </span><span class=\"n\">AssertionError</span><span class=\"p\">(</span><span class=\"s\">\"EC2 instance not found\"</span><span class=\"p\">));</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">var</span><span class=\"w\"> </span><span class=\"n\">tags</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">PulumiTest</span><span class=\"p\">.</span><span class=\"na\">extractValue</span><span class=\"p\">(</span><span class=\"n\">instance</span><span class=\"p\">.</span><span class=\"na\">tags</span><span class=\"p\">())</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">orElseThrow</span><span class=\"p\">(()</span><span class=\"w\"> </span><span class=\"o\">-></span><span class=\"w\"> </span><span class=\"k\">new</span><span class=\"w\"> </span><span class=\"n\">AssertionError</span><span class=\"p\">(</span><span class=\"s\">\"Tags must not be null\"</span><span class=\"p\">));</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">assertTrue</span><span class=\"p\">(</span><span class=\"n\">tags</span><span class=\"p\">.</span><span class=\"na\">containsKey</span><span class=\"p\">(</span><span class=\"s\">\"Environment\"</span><span class=\"p\">),</span><span class=\"w\"> </span><span class=\"s\">\"Missing 'Environment' tag\"</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">assertTrue</span><span class=\"p\">(</span><span class=\"n\">tags</span><span class=\"p\">.</span><span class=\"na\">containsKey</span><span class=\"p\">(</span><span class=\"s\">\"Name\"</span><span class=\"p\">),</span><span class=\"w\"> </span><span class=\"s\">\"Missing 'Name' tag\"</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nd\">@Test</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kt\">void</span><span class=\"w\"> </span><span class=\"nf\">noPublicSshExposed</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">var</span><span class=\"w\"> </span><span class=\"n\">result</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">PulumiTest</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">withMocks</span><span class=\"p\">(</span><span class=\"k\">new</span><span class=\"w\"> </span><span class=\"n\">MyMocks</span><span class=\"p\">())</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">withOptions</span><span class=\"p\">(</span><span class=\"n\">TestOptions</span><span class=\"p\">.</span><span class=\"na\">builder</span><span class=\"p\">()</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">projectName</span><span class=\"p\">(</span><span class=\"s\">\"project\"</span><span class=\"p\">).</span><span class=\"na\">stackName</span><span class=\"p\">(</span><span class=\"s\">\"stack\"</span><span class=\"p\">).</span><span class=\"na\">preview</span><span class=\"p\">(</span><span class=\"kc\">false</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">build</span><span class=\"p\">())</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">runTest</span><span class=\"p\">(</span><span class=\"n\">ctx</span><span class=\"w\"> </span><span class=\"o\">-></span><span class=\"w\"> </span><span class=\"k\">new</span><span class=\"w\"> </span><span class=\"n\">Infra</span><span class=\"p\">());</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">var</span><span class=\"w\"> </span><span class=\"n\">group</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">result</span><span class=\"p\">.</span><span class=\"na\">resources</span><span class=\"p\">().</span><span class=\"na\">stream</span><span class=\"p\">()</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">filter</span><span class=\"p\">(</span><span class=\"n\">r</span><span class=\"w\"> </span><span class=\"o\">-></span><span class=\"w\"> </span><span class=\"n\">r</span><span class=\"w\"> </span><span class=\"k\">instanceof</span><span class=\"w\"> </span><span class=\"n\">SecurityGroup</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">map</span><span class=\"p\">(</span><span class=\"n\">r</span><span class=\"w\"> </span><span class=\"o\">-></span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">SecurityGroup</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"n\">r</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">findFirst</span><span class=\"p\">()</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">orElseThrow</span><span class=\"p\">(()</span><span class=\"w\"> </span><span class=\"o\">-></span><span class=\"w\"> </span><span class=\"k\">new</span><span class=\"w\"> </span><span class=\"n\">AssertionError</span><span class=\"p\">(</span><span class=\"s\">\"Security group not found\"</span><span class=\"p\">));</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">var</span><span class=\"w\"> </span><span class=\"n\">rules</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">PulumiTest</span><span class=\"p\">.</span><span class=\"na\">extractValue</span><span class=\"p\">(</span><span class=\"n\">group</span><span class=\"p\">.</span><span class=\"na\">ingress</span><span class=\"p\">());</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">for</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"kd\">var</span><span class=\"w\"> </span><span class=\"n\">rule</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"n\">rules</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">for</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"kd\">var</span><span class=\"w\"> </span><span class=\"n\">cidr</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"n\">rule</span><span class=\"p\">.</span><span class=\"na\">cidrBlocks</span><span class=\"p\">())</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">assertNotEquals</span><span class=\"p\">(</span><span class=\"s\">\"0.0.0.0/0\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">cidr</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"Security group must not expose SSH to the internet\"</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span><span class=\"w\">\n</span></span></span>\n```\n\nRun with: `mvn test` (JUnit 5)\n\n### The program under test\n\nBoth examples above test this program:\n\n```python\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\"># infra.py</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">pulumi</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">from</span> <span class=\"nn\">pulumi_aws</span> <span class=\"kn\">import</span> <span class=\"n\">ec2</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">group</span> <span class=\"o\">=</span> <span class=\"n\">ec2</span><span class=\"o\">.</span><span class=\"n\">SecurityGroup</span><span class=\"p\">(</span><span class=\"s2\">\"web-secgrp\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">ingress</span><span class=\"o\">=</span><span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"protocol\"</span><span class=\"p\">:</span> <span class=\"s2\">\"tcp\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"from_port\"</span><span class=\"p\">:</span> <span class=\"mi\">80</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"to_port\"</span><span class=\"p\">:</span> <span class=\"mi\">80</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"cidrBlocks\"</span><span class=\"p\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"0.0.0.0/0\"</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">server</span> <span class=\"o\">=</span> <span class=\"n\">ec2</span><span class=\"o\">.</span><span class=\"n\">Instance</span><span class=\"p\">(</span><span class=\"s2\">\"web-server\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">instance_type</span><span class=\"o\">=</span><span class=\"s2\">\"t2.micro\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">ami</span><span class=\"o\">=</span><span class=\"s2\">\"ami-0b0ea68c435eb488d\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">vpc_security_group_ids</span><span class=\"o\">=</span><span class=\"p\">[</span><span class=\"n\">group</span><span class=\"o\">.</span><span class=\"n\">id</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">tags</span><span class=\"o\">=</span><span class=\"p\">{</span><span class=\"s2\">\"Name\"</span><span class=\"p\">:</span> <span class=\"s2\">\"web-server\"</span><span class=\"p\">,</span> <span class=\"s2\">\"Environment\"</span><span class=\"p\">:</span> <span class=\"s2\">\"dev\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span>\n</span></span>\n```\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// index.ts\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">pulumi</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/pulumi\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">aws</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/aws\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">group</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">SecurityGroup</span><span class=\"p\">(</span><span class=\"s2\">\"web-secgrp\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ingress</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">protocol</span><span class=\"o\">:</span> <span class=\"s2\">\"tcp\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">fromPort</span>: <span class=\"kt\">80</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">toPort</span>: <span class=\"kt\">80</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">cidrBlocks</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"0.0.0.0/0\"</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">server</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">Instance</span><span class=\"p\">(</span><span class=\"s2\">\"web-server\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">instanceType</span><span class=\"o\">:</span> <span class=\"s2\">\"t2.micro\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ami</span><span class=\"o\">:</span> <span class=\"s2\">\"ami-0b0ea68c435eb488d\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">vpcSecurityGroupIds</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"nx\">group</span><span class=\"p\">.</span><span class=\"nx\">id</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">tags</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">Name</span><span class=\"o\">:</span> <span class=\"s2\">\"web-server\"</span><span class=\"p\">,</span> <span class=\"nx\">Environment</span><span class=\"o\">:</span> <span class=\"s2\">\"dev\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\n```go\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// main.go</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">package</span><span class=\"w\"> </span><span class=\"nx\">infra_test</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ec2\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">type</span><span class=\"w\"> </span><span class=\"nx\">Infra</span><span class=\"w\"> </span><span class=\"kd\">struct</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">Group</span><span class=\"w\"> </span><span class=\"o\">*</span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">SecurityGroup</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">Server</span><span class=\"w\"> </span><span class=\"o\">*</span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">Instance</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">func</span><span class=\"w\"> </span><span class=\"nf\">NewInfra</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"w\"> </span><span class=\"o\">*</span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">Context</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"nx\">Infra</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"kt\">error</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">group</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nf\">NewSecurityGroup</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"web-secgrp\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"o\">&</span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">SecurityGroupArgs</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">Ingress</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">SecurityGroupIngressArray</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">SecurityGroupIngressArgs</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">Protocol</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">String</span><span class=\"p\">(</span><span class=\"s\">\"tcp\"</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">FromPort</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">Int</span><span class=\"p\">(</span><span class=\"mi\">80</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">ToPort</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">Int</span><span class=\"p\">(</span><span class=\"mi\">80</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">CidrBlocks</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">StringArray</span><span class=\"p\">{</span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">String</span><span class=\"p\">(</span><span class=\"s\">\"0.0.0.0/0\"</span><span class=\"p\">)},</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">},</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">},</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">})</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">!=</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">server</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nf\">NewInstance</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"web-server\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"o\">&</span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">InstanceArgs</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">InstanceType</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">String</span><span class=\"p\">(</span><span class=\"s\">\"t2.micro\"</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">Ami</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">String</span><span class=\"p\">(</span><span class=\"s\">\"ami-0b0ea68c435eb488d\"</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">VpcSecurityGroupIds</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">StringArray</span><span class=\"p\">{</span><span class=\"nx\">group</span><span class=\"p\">.</span><span class=\"nf\">ID</span><span class=\"p\">()},</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">Tags</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">StringMap</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"Name\"</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">String</span><span class=\"p\">(</span><span class=\"s\">\"web-server\"</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"Environment\"</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">String</span><span class=\"p\">(</span><span class=\"s\">\"dev\"</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">},</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">})</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">!=</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"o\">&</span><span class=\"nx\">Infra</span><span class=\"p\">{</span><span class=\"nx\">Group</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">group</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">Server</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">server</span><span class=\"p\">},</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span><span class=\"w\">\n</span></span></span>\n```\n\n```csharp\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// MyStack.cs</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">Pulumi</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">Pulumi.Aws.Ec2</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">Pulumi.Aws.Ec2.Inputs</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">public</span> <span class=\"k\">class</span> <span class=\"nc\">MyStack</span> <span class=\"p\">:</span> <span class=\"n\">Stack</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kd\">public</span> <span class=\"n\">MyStack</span><span class=\"p\">()</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"k\">group</span> <span class=\"p\">=</span> <span class=\"k\">new</span> <span class=\"n\">SecurityGroup</span><span class=\"p\">(</span><span class=\"s\">\"web-secgrp\"</span><span class=\"p\">,</span> <span class=\"k\">new</span> <span class=\"n\">SecurityGroupArgs</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">Ingress</span> <span class=\"p\">=</span> <span class=\"k\">new</span><span class=\"p\">[]</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">new</span> <span class=\"n\">SecurityGroupIngressArgs</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">Protocol</span> <span class=\"p\">=</span> <span class=\"s\">\"tcp\"</span><span class=\"p\">,</span> <span class=\"n\">FromPort</span> <span class=\"p\">=</span> <span class=\"m\">80</span><span class=\"p\">,</span> <span class=\"n\">ToPort</span> <span class=\"p\">=</span> <span class=\"m\">80</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">CidrBlocks</span> <span class=\"p\">=</span> <span class=\"k\">new</span><span class=\"p\">[]</span> <span class=\"p\">{</span> <span class=\"s\">\"0.0.0.0/0\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"n\">server</span> <span class=\"p\">=</span> <span class=\"k\">new</span> <span class=\"n\">Instance</span><span class=\"p\">(</span><span class=\"s\">\"web-server\"</span><span class=\"p\">,</span> <span class=\"k\">new</span> <span class=\"n\">InstanceArgs</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">InstanceType</span> <span class=\"p\">=</span> <span class=\"s\">\"t2.micro\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">Ami</span> <span class=\"p\">=</span> <span class=\"s\">\"ami-0b0ea68c435eb488d\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">VpcSecurityGroupIds</span> <span class=\"p\">=</span> <span class=\"k\">new</span><span class=\"p\">[]</span> <span class=\"p\">{</span> <span class=\"k\">group</span><span class=\"p\">.</span><span class=\"n\">Id</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">Tags</span> <span class=\"p\">=</span> <span class=\"k\">new</span> <span class=\"n\">InputMap</span><span class=\"p\"><</span><span class=\"kt\">string</span><span class=\"p\">></span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span> <span class=\"s\">\"Name\"</span><span class=\"p\">,</span> <span class=\"s\">\"web-server\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span> <span class=\"s\">\"Environment\"</span><span class=\"p\">,</span> <span class=\"s\">\"dev\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span>\n```\n\n```java\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Infra.java</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">package</span><span class=\"w\"> </span><span class=\"nn\">myproject</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.Context</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.Pulumi</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.aws.ec2.SecurityGroup</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.aws.ec2.SecurityGroupArgs</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.aws.ec2.Instance</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.aws.ec2.InstanceArgs</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.aws.ec2.inputs.SecurityGroupIngressArgs</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">java.util.List</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">java.util.Map</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">public</span><span class=\"w\"> </span><span class=\"kd\">class</span> <span class=\"nc\">Infra</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">public</span><span class=\"w\"> </span><span class=\"kd\">final</span><span class=\"w\"> </span><span class=\"n\">SecurityGroup</span><span class=\"w\"> </span><span class=\"n\">group</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">public</span><span class=\"w\"> </span><span class=\"kd\">final</span><span class=\"w\"> </span><span class=\"n\">Instance</span><span class=\"w\"> </span><span class=\"n\">server</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">public</span><span class=\"w\"> </span><span class=\"nf\">Infra</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">this</span><span class=\"p\">.</span><span class=\"na\">group</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"k\">new</span><span class=\"w\"> </span><span class=\"n\">SecurityGroup</span><span class=\"p\">(</span><span class=\"s\">\"web-secgrp\"</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">SecurityGroupArgs</span><span class=\"p\">.</span><span class=\"na\">builder</span><span class=\"p\">()</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">ingress</span><span class=\"p\">(</span><span class=\"n\">SecurityGroupIngressArgs</span><span class=\"p\">.</span><span class=\"na\">builder</span><span class=\"p\">()</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">protocol</span><span class=\"p\">(</span><span class=\"s\">\"tcp\"</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">fromPort</span><span class=\"p\">(</span><span class=\"n\">80</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">toPort</span><span class=\"p\">(</span><span class=\"n\">80</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">cidrBlocks</span><span class=\"p\">(</span><span class=\"s\">\"0.0.0.0/0\"</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">build</span><span class=\"p\">())</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">build</span><span class=\"p\">());</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">this</span><span class=\"p\">.</span><span class=\"na\">server</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"k\">new</span><span class=\"w\"> </span><span class=\"n\">Instance</span><span class=\"p\">(</span><span class=\"s\">\"web-server\"</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">InstanceArgs</span><span class=\"p\">.</span><span class=\"na\">builder</span><span class=\"p\">()</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">instanceType</span><span class=\"p\">(</span><span class=\"s\">\"t2.micro\"</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">ami</span><span class=\"p\">(</span><span class=\"s\">\"ami-0b0ea68c435eb488d\"</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">vpcSecurityGroupIds</span><span class=\"p\">(</span><span class=\"n\">group</span><span class=\"p\">.</span><span class=\"na\">id</span><span class=\"p\">().</span><span class=\"na\">applyValue</span><span class=\"p\">(</span><span class=\"n\">List</span><span class=\"p\">::</span><span class=\"n\">of</span><span class=\"p\">))</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">tags</span><span class=\"p\">(</span><span class=\"n\">Map</span><span class=\"p\">.</span><span class=\"na\">of</span><span class=\"p\">(</span><span class=\"s\">\"Name\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"web-server\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"Environment\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"dev\"</span><span class=\"p\">))</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">build</span><span class=\"p\">());</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span><span class=\"w\">\n</span></span></span>\n```\n\n> **Key detail:** State property keys in the mock’s `new_resource`/`newResource` return value must be **camelCase** (`cidrBlocks`, not `cidr_blocks`), regardless of the language you’re writing in. This is how Pulumi serializes properties internally.\n\n## How do you run integration tests against real cloud resources?\n\nUnit tests tell you whether your resource definitions are correct. Integration tests tell you whether your infrastructure actually works. Integration tests deploy real cloud resources, run assertions against them, and then destroy everything—giving you end-to-end confidence at the cost of actual cloud spend and time.\n\n### Using the Automation API\n\nThe [Pulumi Automation API](https://www.pulumi.com/docs/iac/concepts/automation-api/) lets you drive `pulumi up`, `pulumi destroy`, and every other Pulumi CLI operation programmatically, from within a test. You define the stack inline (using a function that runs your Pulumi program) or point it at an existing project directory.\n\n```python\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\"># test_integration.py</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">pytest</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">pulumi</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">pulumi_aws</span> <span class=\"k\">as</span> <span class=\"nn\">aws</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">from</span> <span class=\"nn\">pulumi</span> <span class=\"kn\">import</span> <span class=\"n\">automation</span> <span class=\"k\">as</span> <span class=\"n\">auto</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">def</span> <span class=\"nf\">create_bucket_program</span><span class=\"p\">():</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"\"\"The Pulumi program to deploy during the test.\"\"\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">bucket</span> <span class=\"o\">=</span> <span class=\"n\">aws</span><span class=\"o\">.</span><span class=\"n\">s3</span><span class=\"o\">.</span><span class=\"n\">Bucket</span><span class=\"p\">(</span><span class=\"s2\">\"test-bucket\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">tags</span><span class=\"o\">=</span><span class=\"p\">{</span><span class=\"s2\">\"Environment\"</span><span class=\"p\">:</span> <span class=\"s2\">\"test\"</span><span class=\"p\">,</span> <span class=\"s2\">\"ManagedBy\"</span><span class=\"p\">:</span> <span class=\"s2\">\"pulumi\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">export</span><span class=\"p\">(</span><span class=\"s2\">\"bucket_name\"</span><span class=\"p\">,</span> <span class=\"n\">bucket</span><span class=\"o\">.</span><span class=\"n\">id</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">export</span><span class=\"p\">(</span><span class=\"s2\">\"bucket_arn\"</span><span class=\"p\">,</span> <span class=\"n\">bucket</span><span class=\"o\">.</span><span class=\"n\">arn</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"nd\">@pytest.fixture</span><span class=\"p\">(</span><span class=\"n\">scope</span><span class=\"o\">=</span><span class=\"s2\">\"module\"</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">def</span> <span class=\"nf\">deployed_stack</span><span class=\"p\">():</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"\"\"Deploy the stack for the test module and destroy it when done.\"\"\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">stack</span> <span class=\"o\">=</span> <span class=\"n\">auto</span><span class=\"o\">.</span><span class=\"n\">create_or_select_stack</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">stack_name</span><span class=\"o\">=</span><span class=\"s2\">\"integration-test\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">project_name</span><span class=\"o\">=</span><span class=\"s2\">\"bucket-test\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">program</span><span class=\"o\">=</span><span class=\"n\">create_bucket_program</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">stack</span><span class=\"o\">.</span><span class=\"n\">set_config</span><span class=\"p\">(</span><span class=\"s2\">\"aws:region\"</span><span class=\"p\">,</span> <span class=\"n\">auto</span><span class=\"o\">.</span><span class=\"n\">ConfigValue</span><span class=\"p\">(</span><span class=\"n\">value</span><span class=\"o\">=</span><span class=\"s2\">\"us-west-2\"</span><span class=\"p\">))</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\"># Install provider plugins</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">stack</span><span class=\"o\">.</span><span class=\"n\">workspace</span><span class=\"o\">.</span><span class=\"n\">install_plugin</span><span class=\"p\">(</span><span class=\"s2\">\"aws\"</span><span class=\"p\">,</span> <span class=\"s2\">\"v7.34.0\"</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\"># Deploy</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">up_result</span> <span class=\"o\">=</span> <span class=\"n\">stack</span><span class=\"o\">.</span><span class=\"n\">up</span><span class=\"p\">(</span><span class=\"n\">on_output</span><span class=\"o\">=</span><span class=\"nb\">print</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">yield</span> <span class=\"n\">up_result</span><span class=\"o\">.</span><span class=\"n\">outputs</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\"># Teardown: destroy everything after the test module completes</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">stack</span><span class=\"o\">.</span><span class=\"n\">destroy</span><span class=\"p\">(</span><span class=\"n\">on_output</span><span class=\"o\">=</span><span class=\"nb\">print</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">stack</span><span class=\"o\">.</span><span class=\"n\">workspace</span><span class=\"o\">.</span><span class=\"n\">remove_stack</span><span class=\"p\">(</span><span class=\"s2\">\"integration-test\"</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">def</span> <span class=\"nf\">test_bucket_was_created</span><span class=\"p\">(</span><span class=\"n\">deployed_stack</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"\"\"The S3 bucket should have been created with the expected name prefix.\"\"\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">bucket_name</span> <span class=\"o\">=</span> <span class=\"n\">deployed_stack</span><span class=\"p\">[</span><span class=\"s2\">\"bucket_name\"</span><span class=\"p\">]</span><span class=\"o\">.</span><span class=\"n\">value</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">assert</span> <span class=\"n\">bucket_name</span><span class=\"o\">.</span><span class=\"n\">startswith</span><span class=\"p\">(</span><span class=\"s2\">\"test-bucket\"</span><span class=\"p\">),</span> <span class=\"sa\">f</span><span class=\"s2\">\"Unexpected bucket name: </span><span class=\"si\">{</span><span class=\"n\">bucket_name</span><span class=\"si\">}</span><span class=\"s2\">\"</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">def</span> <span class=\"nf\">test_bucket_has_tags</span><span class=\"p\">(</span><span class=\"n\">deployed_stack</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"\"\"Verify the bucket was created with required tags (via AWS SDK).\"\"\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kn\">import</span> <span class=\"nn\">boto3</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">s3</span> <span class=\"o\">=</span> <span class=\"n\">boto3</span><span class=\"o\">.</span><span class=\"n\">client</span><span class=\"p\">(</span><span class=\"s2\">\"s3\"</span><span class=\"p\">,</span> <span class=\"n\">region_name</span><span class=\"o\">=</span><span class=\"s2\">\"us-west-2\"</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">bucket_name</span> <span class=\"o\">=</span> <span class=\"n\">deployed_stack</span><span class=\"p\">[</span><span class=\"s2\">\"bucket_name\"</span><span class=\"p\">]</span><span class=\"o\">.</span><span class=\"n\">value</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">tagging</span> <span class=\"o\">=</span> <span class=\"n\">s3</span><span class=\"o\">.</span><span class=\"n\">get_bucket_tagging</span><span class=\"p\">(</span><span class=\"n\">Bucket</span><span class=\"o\">=</span><span class=\"n\">bucket_name</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">tag_map</span> <span class=\"o\">=</span> <span class=\"p\">{</span><span class=\"n\">t</span><span class=\"p\">[</span><span class=\"s2\">\"Key\"</span><span class=\"p\">]:</span> <span class=\"n\">t</span><span class=\"p\">[</span><span class=\"s2\">\"Value\"</span><span class=\"p\">]</span> <span class=\"k\">for</span> <span class=\"n\">t</span> <span class=\"ow\">in</span> <span class=\"n\">tagging</span><span class=\"p\">[</span><span class=\"s2\">\"TagSet\"</span><span class=\"p\">]}</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">assert</span> <span class=\"n\">tag_map</span><span class=\"o\">.</span><span class=\"n\">get</span><span class=\"p\">(</span><span class=\"s2\">\"Environment\"</span><span class=\"p\">)</span> <span class=\"o\">==</span> <span class=\"s2\">\"test\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">assert</span> <span class=\"n\">tag_map</span><span class=\"o\">.</span><span class=\"n\">get</span><span class=\"p\">(</span><span class=\"s2\">\"ManagedBy\"</span><span class=\"p\">)</span> <span class=\"o\">==</span> <span class=\"s2\">\"pulumi\"</span>\n</span></span>\n```\n\nRun with: `pytest test_integration.py -v`\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// integration.test.ts\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"p\">{</span> <span class=\"nx\">LocalWorkspace</span><span class=\"p\">,</span> <span class=\"nx\">ConfigValue</span><span class=\"p\">,</span> <span class=\"nx\">UpResult</span> <span class=\"p\">}</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/pulumi/automation\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">aws</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/aws\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">pulumi</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/pulumi\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"p\">{</span> <span class=\"nx\">expect</span> <span class=\"p\">}</span> <span class=\"kr\">from</span> <span class=\"s2\">\"chai\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"nx\">describe</span><span class=\"p\">(</span><span class=\"s2\">\"S3 Bucket Integration\"</span><span class=\"p\">,</span> <span class=\"kd\">function</span> <span class=\"p\">()</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">this</span><span class=\"p\">.</span><span class=\"nx\">timeout</span><span class=\"p\">(</span><span class=\"mi\">300</span><span class=\"nx\">_000</span><span class=\"p\">);</span> <span class=\"c1\">// 5 min — real deploys take time\n</span></span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kd\">let</span> <span class=\"nx\">outputs</span>: <span class=\"kt\">UpResult</span><span class=\"p\">[</span><span class=\"s2\">\"outputs\"</span><span class=\"p\">];</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kd\">let</span> <span class=\"nx\">stackName</span> <span class=\"o\">=</span> <span class=\"s2\">\"integration-test-ts\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">before</span><span class=\"p\">(</span><span class=\"kr\">async</span> <span class=\"kd\">function</span> <span class=\"p\">()</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">const</span> <span class=\"nx\">stack</span> <span class=\"o\">=</span> <span class=\"k\">await</span> <span class=\"nx\">LocalWorkspace</span><span class=\"p\">.</span><span class=\"nx\">createOrSelectStack</span><span class=\"p\">({</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">stackName</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">projectName</span><span class=\"o\">:</span> <span class=\"s2\">\"bucket-test\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">program</span>: <span class=\"kt\">async</span> <span class=\"p\">()</span> <span class=\"o\">=></span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">const</span> <span class=\"nx\">bucket</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">s3</span><span class=\"p\">.</span><span class=\"nx\">Bucket</span><span class=\"p\">(</span><span class=\"s2\">\"test-bucket\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">tags</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">Environment</span><span class=\"o\">:</span> <span class=\"s2\">\"test\"</span><span class=\"p\">,</span> <span class=\"nx\">ManagedBy</span><span class=\"o\">:</span> <span class=\"s2\">\"pulumi\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">return</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">bucketName</span>: <span class=\"kt\">bucket.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">bucketArn</span>: <span class=\"kt\">bucket.arn</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">};</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">await</span> <span class=\"nx\">stack</span><span class=\"p\">.</span><span class=\"nx\">setConfig</span><span class=\"p\">(</span><span class=\"s2\">\"aws:region\"</span><span class=\"p\">,</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"us-west-2\"</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">await</span> <span class=\"nx\">stack</span><span class=\"p\">.</span><span class=\"nx\">workspace</span><span class=\"p\">.</span><span class=\"nx\">installPlugin</span><span class=\"p\">(</span><span class=\"s2\">\"aws\"</span><span class=\"p\">,</span> <span class=\"s2\">\"v7.34.0\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">const</span> <span class=\"nx\">upResult</span> <span class=\"o\">=</span> <span class=\"k\">await</span> <span class=\"nx\">stack</span><span class=\"p\">.</span><span class=\"nx\">up</span><span class=\"p\">({</span> <span class=\"nx\">onOutput</span>: <span class=\"kt\">console.log</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">outputs</span> <span class=\"o\">=</span> <span class=\"nx\">upResult</span><span class=\"p\">.</span><span class=\"nx\">outputs</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">after</span><span class=\"p\">(</span><span class=\"kr\">async</span> <span class=\"kd\">function</span> <span class=\"p\">()</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">const</span> <span class=\"nx\">stack</span> <span class=\"o\">=</span> <span class=\"k\">await</span> <span class=\"nx\">LocalWorkspace</span><span class=\"p\">.</span><span class=\"nx\">selectStack</span><span class=\"p\">({</span> <span class=\"nx\">stackName</span><span class=\"p\">,</span> <span class=\"nx\">projectName</span><span class=\"o\">:</span> <span class=\"s2\">\"bucket-test\"</span><span class=\"p\">,</span> <span class=\"nx\">program</span>: <span class=\"kt\">async</span> <span class=\"p\">()</span> <span class=\"o\">=></span> <span class=\"p\">{}</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">await</span> <span class=\"nx\">stack</span><span class=\"p\">.</span><span class=\"nx\">destroy</span><span class=\"p\">({</span> <span class=\"nx\">onOutput</span>: <span class=\"kt\">console.log</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">await</span> <span class=\"nx\">stack</span><span class=\"p\">.</span><span class=\"nx\">workspace</span><span class=\"p\">.</span><span class=\"nx\">removeStack</span><span class=\"p\">(</span><span class=\"nx\">stackName</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">it</span><span class=\"p\">(</span><span class=\"s2\">\"creates a bucket with the right name prefix\"</span><span class=\"p\">,</span> <span class=\"kd\">function</span> <span class=\"p\">()</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">const</span> <span class=\"nx\">name</span> <span class=\"o\">=</span> <span class=\"nx\">outputs</span><span class=\"p\">[</span><span class=\"s2\">\"bucketName\"</span><span class=\"p\">].</span><span class=\"nx\">value</span> <span class=\"kr\">as</span> <span class=\"kt\">string</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">expect</span><span class=\"p\">(</span><span class=\"nx\">name</span><span class=\"p\">).</span><span class=\"nx\">to</span><span class=\"p\">.</span><span class=\"nx\">match</span><span class=\"p\">(</span><span class=\"sr\">/^test-bucket/</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\nRun with: `mocha -r ts-node/register integration.test.ts`\n\n```go\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// integration_test.go</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">package</span><span class=\"w\"> </span><span class=\"nx\">integration_test</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"context\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"fmt\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"os\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"testing\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/s3\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"github.com/pulumi/pulumi/sdk/v3/go/auto\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"github.com/pulumi/pulumi/sdk/v3/go/auto/optup\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"github.com/stretchr/testify/assert\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">func</span><span class=\"w\"> </span><span class=\"nf\">TestBucketIntegration</span><span class=\"p\">(</span><span class=\"nx\">t</span><span class=\"w\"> </span><span class=\"o\">*</span><span class=\"nx\">testing</span><span class=\"p\">.</span><span class=\"nx\">T</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">ctx</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"nx\">context</span><span class=\"p\">.</span><span class=\"nf\">Background</span><span class=\"p\">()</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">stack</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"nx\">auto</span><span class=\"p\">.</span><span class=\"nf\">NewStackInlineSource</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"integration-test\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"bucket-test\"</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">func</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"w\"> </span><span class=\"o\">*</span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">Context</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"kt\">error</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">bucket</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"nx\">s3</span><span class=\"p\">.</span><span class=\"nf\">NewBucket</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"test-bucket\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"o\">&</span><span class=\"nx\">s3</span><span class=\"p\">.</span><span class=\"nx\">BucketArgs</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">Tags</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">StringMap</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"Environment\"</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">String</span><span class=\"p\">(</span><span class=\"s\">\"test\"</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"s\">\"ManagedBy\"</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nf\">String</span><span class=\"p\">(</span><span class=\"s\">\"pulumi\"</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">},</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">})</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">!=</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">ctx</span><span class=\"p\">.</span><span class=\"nf\">Export</span><span class=\"p\">(</span><span class=\"s\">\"bucketName\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">bucket</span><span class=\"p\">.</span><span class=\"nf\">ID</span><span class=\"p\">())</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">return</span><span class=\"w\"> </span><span class=\"kc\">nil</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">},</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">assert</span><span class=\"p\">.</span><span class=\"nf\">NoError</span><span class=\"p\">(</span><span class=\"nx\">t</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">_</span><span class=\"w\"> </span><span class=\"p\">=</span><span class=\"w\"> </span><span class=\"nx\">stack</span><span class=\"p\">.</span><span class=\"nf\">SetConfig</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"aws:region\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">auto</span><span class=\"p\">.</span><span class=\"nx\">ConfigValue</span><span class=\"p\">{</span><span class=\"nx\">Value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s\">\"us-west-2\"</span><span class=\"p\">})</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">upResult</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"nx\">stack</span><span class=\"p\">.</span><span class=\"nf\">Up</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">optup</span><span class=\"p\">.</span><span class=\"nf\">ProgressStreams</span><span class=\"p\">(</span><span class=\"nx\">os</span><span class=\"p\">.</span><span class=\"nx\">Stdout</span><span class=\"p\">))</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">assert</span><span class=\"p\">.</span><span class=\"nf\">NoError</span><span class=\"p\">(</span><span class=\"nx\">t</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">err</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">t</span><span class=\"p\">.</span><span class=\"nf\">Cleanup</span><span class=\"p\">(</span><span class=\"kd\">func</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">_</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">_</span><span class=\"w\"> </span><span class=\"p\">=</span><span class=\"w\"> </span><span class=\"nx\">stack</span><span class=\"p\">.</span><span class=\"nf\">Destroy</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">_</span><span class=\"w\"> </span><span class=\"p\">=</span><span class=\"w\"> </span><span class=\"nx\">stack</span><span class=\"p\">.</span><span class=\"nf\">Workspace</span><span class=\"p\">().</span><span class=\"nf\">RemoveStack</span><span class=\"p\">(</span><span class=\"nx\">ctx</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"integration-test\"</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">})</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">name</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"nx\">fmt</span><span class=\"p\">.</span><span class=\"nf\">Sprintf</span><span class=\"p\">(</span><span class=\"s\">\"%v\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">upResult</span><span class=\"p\">.</span><span class=\"nx\">Outputs</span><span class=\"p\">[</span><span class=\"s\">\"bucketName\"</span><span class=\"p\">].</span><span class=\"nx\">Value</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">assert</span><span class=\"p\">.</span><span class=\"nf\">Contains</span><span class=\"p\">(</span><span class=\"nx\">t</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">name</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"test-bucket\"</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span><span class=\"w\">\n</span></span></span>\n```\n\nRun with: `go test ./... -v -timeout 10m`\n\n```csharp\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// IntegrationTests.cs</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">System</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">System.Threading.Tasks</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">Pulumi.Automation</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">using</span> <span class=\"nn\">Xunit</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">public</span> <span class=\"k\">class</span> <span class=\"nc\">IntegrationTests</span> <span class=\"p\">:</span> <span class=\"n\">IAsyncLifetime</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kd\">private</span> <span class=\"n\">WorkspaceStack</span> <span class=\"n\">_stack</span> <span class=\"p\">=</span> <span class=\"kc\">null</span><span class=\"p\">!;</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kd\">private</span> <span class=\"n\">UpResult</span> <span class=\"n\">_upResult</span> <span class=\"p\">=</span> <span class=\"kc\">null</span><span class=\"p\">!;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kd\">public</span> <span class=\"kd\">async</span> <span class=\"n\">Task</span> <span class=\"n\">InitializeAsync</span><span class=\"p\">()</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"n\">program</span> <span class=\"p\">=</span> <span class=\"n\">PulumiFn</span><span class=\"p\">.</span><span class=\"n\">Create</span><span class=\"p\">(()</span> <span class=\"p\">=></span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"n\">bucket</span> <span class=\"p\">=</span> <span class=\"k\">new</span> <span class=\"n\">Pulumi</span><span class=\"p\">.</span><span class=\"n\">Aws</span><span class=\"p\">.</span><span class=\"n\">S3</span><span class=\"p\">.</span><span class=\"n\">Bucket</span><span class=\"p\">(</span><span class=\"s\">\"test-bucket\"</span><span class=\"p\">,</span> <span class=\"k\">new</span><span class=\"p\">()</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">Tags</span> <span class=\"p\">=</span> <span class=\"k\">new</span><span class=\"p\">()</span> <span class=\"p\">{</span> <span class=\"p\">[</span><span class=\"s\">\"Environment\"</span><span class=\"p\">]</span> <span class=\"p\">=</span> <span class=\"s\">\"test\"</span><span class=\"p\">,</span> <span class=\"p\">[</span><span class=\"s\">\"ManagedBy\"</span><span class=\"p\">]</span> <span class=\"p\">=</span> <span class=\"s\">\"pulumi\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">return</span> <span class=\"k\">new</span> <span class=\"n\">System</span><span class=\"p\">.</span><span class=\"n\">Collections</span><span class=\"p\">.</span><span class=\"n\">Generic</span><span class=\"p\">.</span><span class=\"n\">Dictionary</span><span class=\"p\"><</span><span class=\"kt\">string</span><span class=\"p\">,</span> <span class=\"kt\">object?</span><span class=\"p\">></span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"na\"> [\"bucketName\"]</span> <span class=\"p\">=</span> <span class=\"n\">bucket</span><span class=\"p\">.</span><span class=\"n\">Id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">};</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">_stack</span> <span class=\"p\">=</span> <span class=\"k\">await</span> <span class=\"n\">LocalWorkspace</span><span class=\"p\">.</span><span class=\"n\">CreateOrSelectStackAsync</span><span class=\"p\">(</span><span class=\"k\">new</span> <span class=\"n\">InlineProgramArgs</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">projectName</span><span class=\"p\">:</span> <span class=\"s\">\"bucket-test\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">stackName</span><span class=\"p\">:</span> <span class=\"s\">\"integration-test\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">program</span><span class=\"p\">:</span> <span class=\"n\">program</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">));</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">await</span> <span class=\"n\">_stack</span><span class=\"p\">.</span><span class=\"n\">SetConfigAsync</span><span class=\"p\">(</span><span class=\"s\">\"aws:region\"</span><span class=\"p\">,</span> <span class=\"k\">new</span> <span class=\"n\">ConfigValue</span><span class=\"p\">(</span><span class=\"s\">\"us-west-2\"</span><span class=\"p\">));</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">_upResult</span> <span class=\"p\">=</span> <span class=\"k\">await</span> <span class=\"n\">_stack</span><span class=\"p\">.</span><span class=\"n\">UpAsync</span><span class=\"p\">(</span><span class=\"k\">new</span> <span class=\"n\">UpOptions</span> <span class=\"p\">{</span> <span class=\"n\">OnStandardOutput</span> <span class=\"p\">=</span> <span class=\"n\">Console</span><span class=\"p\">.</span><span class=\"n\">WriteLine</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kd\">public</span> <span class=\"kd\">async</span> <span class=\"n\">Task</span> <span class=\"n\">DisposeAsync</span><span class=\"p\">()</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">await</span> <span class=\"n\">_stack</span><span class=\"p\">.</span><span class=\"n\">DestroyAsync</span><span class=\"p\">(</span><span class=\"k\">new</span> <span class=\"n\">DestroyOptions</span> <span class=\"p\">{</span> <span class=\"n\">OnStandardOutput</span> <span class=\"p\">=</span> <span class=\"n\">Console</span><span class=\"p\">.</span><span class=\"n\">WriteLine</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">await</span> <span class=\"n\">_stack</span><span class=\"p\">.</span><span class=\"n\">Workspace</span><span class=\"p\">.</span><span class=\"n\">RemoveStackAsync</span><span class=\"p\">(</span><span class=\"s\">\"integration-test\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"na\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"na\"> [Fact]</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kd\">public</span> <span class=\"k\">void</span> <span class=\"n\">BucketNameHasExpectedPrefix</span><span class=\"p\">()</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kt\">var</span> <span class=\"n\">name</span> <span class=\"p\">=</span> <span class=\"n\">_upResult</span><span class=\"p\">.</span><span class=\"n\">Outputs</span><span class=\"p\">[</span><span class=\"s\">\"bucketName\"</span><span class=\"p\">].</span><span class=\"n\">Value</span><span class=\"p\">.</span><span class=\"n\">ToString</span><span class=\"p\">();</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">Assert</span><span class=\"p\">.</span><span class=\"n\">StartsWith</span><span class=\"p\">(</span><span class=\"s\">\"test-bucket\"</span><span class=\"p\">,</span> <span class=\"n\">name</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span>\n```\n\nRun with: `dotnet test`\n\n```java\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// IntegrationTest.java</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">package</span><span class=\"w\"> </span><span class=\"nn\">myproject</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.Context</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.automation.LocalWorkspace</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.automation.UpOptions</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.automation.DestroyOptions</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.automation.ConfigValue</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">com.pulumi.automation.WorkspaceStack</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">org.junit.jupiter.api.*</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span><span class=\"w\"> </span><span class=\"nn\">java.util.Map</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import static</span><span class=\"w\"> </span><span class=\"nn\">org.junit.jupiter.api.Assertions.*</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nd\">@TestInstance</span><span class=\"p\">(</span><span class=\"n\">TestInstance</span><span class=\"p\">.</span><span class=\"na\">Lifecycle</span><span class=\"p\">.</span><span class=\"na\">PER_CLASS</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kd\">class</span> <span class=\"nc\">IntegrationTest</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">private</span><span class=\"w\"> </span><span class=\"kd\">static</span><span class=\"w\"> </span><span class=\"kd\">final</span><span class=\"w\"> </span><span class=\"n\">String</span><span class=\"w\"> </span><span class=\"n\">PROJECT_NAME</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"s\">\"bucket_test_project\"</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">private</span><span class=\"w\"> </span><span class=\"kd\">static</span><span class=\"w\"> </span><span class=\"kd\">final</span><span class=\"w\"> </span><span class=\"n\">String</span><span class=\"w\"> </span><span class=\"n\">STACK_NAME</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"s\">\"integration-test\"</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">private</span><span class=\"w\"> </span><span class=\"n\">WorkspaceStack</span><span class=\"w\"> </span><span class=\"n\">stack</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">private</span><span class=\"w\"> </span><span class=\"n\">Map</span><span class=\"o\"><</span><span class=\"n\">String</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">com</span><span class=\"p\">.</span><span class=\"na\">pulumi</span><span class=\"p\">.</span><span class=\"na\">automation</span><span class=\"p\">.</span><span class=\"na\">OutputValue</span><span class=\"o\">></span><span class=\"w\"> </span><span class=\"n\">outputs</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nd\">@BeforeAll</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kt\">void</span><span class=\"w\"> </span><span class=\"nf\">setUp</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"kd\">throws</span><span class=\"w\"> </span><span class=\"n\">Exception</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">stack</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">LocalWorkspace</span><span class=\"p\">.</span><span class=\"na\">createOrSelectStack</span><span class=\"p\">(</span><span class=\"n\">PROJECT_NAME</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">STACK_NAME</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">Context</span><span class=\"w\"> </span><span class=\"n\">ctx</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"o\">-></span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">var</span><span class=\"w\"> </span><span class=\"n\">bucket</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"k\">new</span><span class=\"w\"> </span><span class=\"n\">com</span><span class=\"p\">.</span><span class=\"na\">pulumi</span><span class=\"p\">.</span><span class=\"na\">aws</span><span class=\"p\">.</span><span class=\"na\">s3</span><span class=\"p\">.</span><span class=\"na\">Bucket</span><span class=\"p\">(</span><span class=\"s\">\"test-bucket\"</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">com</span><span class=\"p\">.</span><span class=\"na\">pulumi</span><span class=\"p\">.</span><span class=\"na\">aws</span><span class=\"p\">.</span><span class=\"na\">s3</span><span class=\"p\">.</span><span class=\"na\">BucketArgs</span><span class=\"p\">.</span><span class=\"na\">builder</span><span class=\"p\">()</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">tags</span><span class=\"p\">(</span><span class=\"n\">Map</span><span class=\"p\">.</span><span class=\"na\">of</span><span class=\"p\">(</span><span class=\"s\">\"Environment\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"test\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"ManagedBy\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"pulumi\"</span><span class=\"p\">))</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">.</span><span class=\"na\">build</span><span class=\"p\">());</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">ctx</span><span class=\"p\">.</span><span class=\"na\">export</span><span class=\"p\">(</span><span class=\"s\">\"bucketName\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">bucket</span><span class=\"p\">.</span><span class=\"na\">id</span><span class=\"p\">());</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">});</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">stack</span><span class=\"p\">.</span><span class=\"na\">workspace</span><span class=\"p\">().</span><span class=\"na\">installPlugin</span><span class=\"p\">(</span><span class=\"s\">\"aws\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"v7.34.0\"</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">stack</span><span class=\"p\">.</span><span class=\"na\">setConfig</span><span class=\"p\">(</span><span class=\"s\">\"aws:region\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"k\">new</span><span class=\"w\"> </span><span class=\"n\">ConfigValue</span><span class=\"p\">(</span><span class=\"s\">\"us-west-2\"</span><span class=\"p\">));</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">var</span><span class=\"w\"> </span><span class=\"n\">upResult</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">stack</span><span class=\"p\">.</span><span class=\"na\">up</span><span class=\"p\">(</span><span class=\"n\">UpOptions</span><span class=\"p\">.</span><span class=\"na\">builder</span><span class=\"p\">().</span><span class=\"na\">onStandardOutput</span><span class=\"p\">(</span><span class=\"n\">System</span><span class=\"p\">.</span><span class=\"na\">out</span><span class=\"p\">::</span><span class=\"n\">println</span><span class=\"p\">).</span><span class=\"na\">build</span><span class=\"p\">());</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">outputs</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">upResult</span><span class=\"p\">.</span><span class=\"na\">outputs</span><span class=\"p\">();</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nd\">@AfterAll</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kt\">void</span><span class=\"w\"> </span><span class=\"nf\">tearDown</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"kd\">throws</span><span class=\"w\"> </span><span class=\"n\">Exception</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">if</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">stack</span><span class=\"w\"> </span><span class=\"o\">!=</span><span class=\"w\"> </span><span class=\"kc\">null</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">stack</span><span class=\"p\">.</span><span class=\"na\">destroy</span><span class=\"p\">(</span><span class=\"n\">DestroyOptions</span><span class=\"p\">.</span><span class=\"na\">builder</span><span class=\"p\">().</span><span class=\"na\">onStandardOutput</span><span class=\"p\">(</span><span class=\"n\">System</span><span class=\"p\">.</span><span class=\"na\">out</span><span class=\"p\">::</span><span class=\"n\">println</span><span class=\"p\">).</span><span class=\"na\">build</span><span class=\"p\">());</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">stack</span><span class=\"p\">.</span><span class=\"na\">workspace</span><span class=\"p\">().</span><span class=\"na\">removeStack</span><span class=\"p\">(</span><span class=\"n\">STACK_NAME</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nd\">@Test</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kt\">void</span><span class=\"w\"> </span><span class=\"nf\">bucketNameHasExpectedPrefix</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"kd\">var</span><span class=\"w\"> </span><span class=\"n\">name</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">String</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"n\">outputs</span><span class=\"p\">.</span><span class=\"na\">get</span><span class=\"p\">(</span><span class=\"s\">\"bucketName\"</span><span class=\"p\">).</span><span class=\"na\">value</span><span class=\"p\">();</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"n\">assertTrue</span><span class=\"p\">(</span><span class=\"n\">name</span><span class=\"p\">.</span><span class=\"na\">startsWith</span><span class=\"p\">(</span><span class=\"s\">\"test-bucket\"</span><span class=\"p\">),</span><span class=\"w\"> </span><span class=\"s\">\"Unexpected bucket name: \"</span><span class=\"w\"> </span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"n\">name</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span><span class=\"w\">\n</span></span></span>\n```\n\nRun with: `mvn test`\n\n### Using the Go integration framework\n\nIf you prefer Go-based integration tests, Pulumi ships a purpose-built Go test framework at `github.com/pulumi/pulumi/pkg/v3/testing/integration`. It handles the full `create → validate → destroy` lifecycle for any Pulumi program (written in any language), with an `ExtraRuntimeValidation` hook for assertions against deployed state.\n\n```go\n<span class=\"line\"><span class=\"cl\"><span class=\"kd\">func</span><span class=\"w\"> </span><span class=\"nf\">TestS3Website</span><span class=\"p\">(</span><span class=\"nx\">t</span><span class=\"w\"> </span><span class=\"o\">*</span><span class=\"nx\">testing</span><span class=\"p\">.</span><span class=\"nx\">T</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">integration</span><span class=\"p\">.</span><span class=\"nf\">ProgramTest</span><span class=\"p\">(</span><span class=\"nx\">t</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"o\">&</span><span class=\"nx\">integration</span><span class=\"p\">.</span><span class=\"nx\">ProgramTestOptions</span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">Dir</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"nx\">path</span><span class=\"p\">.</span><span class=\"nf\">Join</span><span class=\"p\">(</span><span class=\"s\">\"..\"</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"s\">\"my-s3-website\"</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">Config</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kd\">map</span><span class=\"p\">[</span><span class=\"kt\">string</span><span class=\"p\">]</span><span class=\"kt\">string</span><span class=\"p\">{</span><span class=\"s\">\"aws:region\"</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s\">\"us-west-2\"</span><span class=\"p\">},</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">ExtraRuntimeValidation</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kd\">func</span><span class=\"p\">(</span><span class=\"nx\">t</span><span class=\"w\"> </span><span class=\"o\">*</span><span class=\"nx\">testing</span><span class=\"p\">.</span><span class=\"nx\">T</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">stack</span><span class=\"w\"> </span><span class=\"nx\">integration</span><span class=\"p\">.</span><span class=\"nx\">RuntimeValidationStackInfo</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"p\">{</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">url</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"nx\">stack</span><span class=\"p\">.</span><span class=\"nx\">Outputs</span><span class=\"p\">[</span><span class=\"s\">\"websiteUrl\"</span><span class=\"p\">].(</span><span class=\"kt\">string</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">resp</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">_</span><span class=\"w\"> </span><span class=\"o\">:=</span><span class=\"w\"> </span><span class=\"nx\">http</span><span class=\"p\">.</span><span class=\"nf\">Get</span><span class=\"p\">(</span><span class=\"s\">\"http://\"</span><span class=\"w\"> </span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"nx\">url</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nx\">assert</span><span class=\"p\">.</span><span class=\"nf\">Equal</span><span class=\"p\">(</span><span class=\"nx\">t</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">200</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"nx\">resp</span><span class=\"p\">.</span><span class=\"nx\">StatusCode</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">},</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"p\">})</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span><span class=\"w\">\n</span></span></span>\n```\n\n> **Best practice:** Always destroy test stacks in a `finally` block or test teardown fixture. Leaked integration test stacks are a common cause of unexpected cloud spend.\n\n## How does policy as code complement IaC testing?\n\nUnit and integration tests validate what your infrastructure *does*. Policy as code enforces what your infrastructure *must* conform to—security standards, tagging conventions, compliance requirements—and it runs automatically on every `pulumi preview` and `pulumi up`.\n\nPulumi Policies are currently supported in Python and TypeScript. You write policies in the same language as the rest of your stack, without learning a DSL. A `mandatory` policy blocks a deployment entirely when violated; an `advisory` policy surfaces a warning but allows the deploy to proceed.\n\n```python\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\"># __main__.py in your policy pack directory</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">from</span> <span class=\"nn\">pulumi_policy</span> <span class=\"kn\">import</span> <span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">EnforcementLevel</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">PolicyPack</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">ReportViolation</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">ResourceValidationArgs</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">ResourceValidationPolicy</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">REQUIRED_TAGS</span> <span class=\"o\">=</span> <span class=\"p\">{</span><span class=\"s2\">\"Name\"</span><span class=\"p\">,</span> <span class=\"s2\">\"Environment\"</span><span class=\"p\">,</span> <span class=\"s2\">\"Owner\"</span><span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">def</span> <span class=\"nf\">require_tags</span><span class=\"p\">(</span><span class=\"n\">args</span><span class=\"p\">:</span> <span class=\"n\">ResourceValidationArgs</span><span class=\"p\">,</span> <span class=\"n\">report_violation</span><span class=\"p\">:</span> <span class=\"n\">ReportViolation</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">if</span> <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">resource_type</span><span class=\"o\">.</span><span class=\"n\">startswith</span><span class=\"p\">(</span><span class=\"s2\">\"aws:\"</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">tags</span> <span class=\"o\">=</span> <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">props</span><span class=\"o\">.</span><span class=\"n\">get</span><span class=\"p\">(</span><span class=\"s2\">\"tags\"</span><span class=\"p\">)</span> <span class=\"ow\">or</span> <span class=\"p\">{}</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">missing</span> <span class=\"o\">=</span> <span class=\"n\">REQUIRED_TAGS</span> <span class=\"o\">-</span> <span class=\"nb\">set</span><span class=\"p\">(</span><span class=\"n\">tags</span><span class=\"o\">.</span><span class=\"n\">keys</span><span class=\"p\">())</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">if</span> <span class=\"n\">missing</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">report_violation</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"sa\">f</span><span class=\"s2\">\"Resource is missing required tags: </span><span class=\"si\">{</span><span class=\"s1\">', '</span><span class=\"o\">.</span><span class=\"n\">join</span><span class=\"p\">(</span><span class=\"nb\">sorted</span><span class=\"p\">(</span><span class=\"n\">missing</span><span class=\"p\">))</span><span class=\"si\">}</span><span class=\"s2\">\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">def</span> <span class=\"nf\">no_public_s3</span><span class=\"p\">(</span><span class=\"n\">args</span><span class=\"p\">:</span> <span class=\"n\">ResourceValidationArgs</span><span class=\"p\">,</span> <span class=\"n\">report_violation</span><span class=\"p\">:</span> <span class=\"n\">ReportViolation</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">if</span> <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">resource_type</span> <span class=\"o\">==</span> <span class=\"s2\">\"aws:s3/bucket:Bucket\"</span><span class=\"p\">:</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">if</span> <span class=\"n\">args</span><span class=\"o\">.</span><span class=\"n\">props</span><span class=\"o\">.</span><span class=\"n\">get</span><span class=\"p\">(</span><span class=\"s2\">\"acl\"</span><span class=\"p\">)</span> <span class=\"ow\">in</span> <span class=\"p\">(</span><span class=\"s2\">\"public-read\"</span><span class=\"p\">,</span> <span class=\"s2\">\"public-read-write\"</span><span class=\"p\">):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">report_violation</span><span class=\"p\">(</span><span class=\"s2\">\"S3 buckets must not have a public ACL.\"</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">PolicyPack</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">name</span><span class=\"o\">=</span><span class=\"s2\">\"aws-security-baseline\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">policies</span><span class=\"o\">=</span><span class=\"p\">[</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">ResourceValidationPolicy</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">name</span><span class=\"o\">=</span><span class=\"s2\">\"require-tags\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">description</span><span class=\"o\">=</span><span class=\"s2\">\"All AWS resources must have Name, Environment, and Owner tags.\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">enforcement_level</span><span class=\"o\">=</span><span class=\"n\">EnforcementLevel</span><span class=\"o\">.</span><span class=\"n\">MANDATORY</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">validate</span><span class=\"o\">=</span><span class=\"n\">require_tags</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">ResourceValidationPolicy</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">name</span><span class=\"o\">=</span><span class=\"s2\">\"no-public-s3\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">description</span><span class=\"o\">=</span><span class=\"s2\">\"S3 buckets must not be publicly accessible via ACL.\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">enforcement_level</span><span class=\"o\">=</span><span class=\"n\">EnforcementLevel</span><span class=\"o\">.</span><span class=\"n\">MANDATORY</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">validate</span><span class=\"o\">=</span><span class=\"n\">no_public_s3</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span>\n</span></span>\n```\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// index.ts in your policy pack directory\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">aws</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/aws\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"p\">{</span> <span class=\"nx\">PolicyPack</span><span class=\"p\">,</span> <span class=\"nx\">validateResourceOfType</span> <span class=\"p\">}</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/policy\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">new</span> <span class=\"nx\">PolicyPack</span><span class=\"p\">(</span><span class=\"s2\">\"aws-security-baseline\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">policies</span><span class=\"o\">:</span> <span class=\"p\">[</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"require-tags\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">description</span><span class=\"o\">:</span> <span class=\"s2\">\"All EC2 instances must have Name, Environment, and Owner tags.\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">enforcementLevel</span><span class=\"o\">:</span> <span class=\"s2\">\"mandatory\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">validateResource</span>: <span class=\"kt\">validateResourceOfType</span><span class=\"p\">(</span><span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">Instance</span><span class=\"p\">,</span> <span class=\"p\">(</span><span class=\"nx\">instance</span><span class=\"p\">,</span> <span class=\"nx\">args</span><span class=\"p\">,</span> <span class=\"nx\">reportViolation</span><span class=\"p\">)</span> <span class=\"o\">=></span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">const</span> <span class=\"nx\">required</span> <span class=\"o\">=</span> <span class=\"p\">[</span><span class=\"s2\">\"Name\"</span><span class=\"p\">,</span> <span class=\"s2\">\"Environment\"</span><span class=\"p\">,</span> <span class=\"s2\">\"Owner\"</span><span class=\"p\">];</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">const</span> <span class=\"nx\">tags</span> <span class=\"o\">=</span> <span class=\"nx\">instance</span><span class=\"p\">.</span><span class=\"nx\">tags</span> <span class=\"o\">||</span> <span class=\"p\">{};</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">for</span> <span class=\"p\">(</span><span class=\"kr\">const</span> <span class=\"nx\">tag</span> <span class=\"k\">of</span> <span class=\"nx\">required</span><span class=\"p\">)</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">if</span> <span class=\"p\">(</span><span class=\"o\">!</span><span class=\"nx\">tags</span><span class=\"p\">[</span><span class=\"nx\">tag</span><span class=\"p\">])</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">reportViolation</span><span class=\"p\">(</span><span class=\"sb\">`Missing required tag: </span><span class=\"si\">${</span><span class=\"nx\">tag</span><span class=\"si\">}</span><span class=\"sb\">`</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}),</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"no-public-s3\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">description</span><span class=\"o\">:</span> <span class=\"s2\">\"S3 buckets must not have a public ACL.\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">enforcementLevel</span><span class=\"o\">:</span> <span class=\"s2\">\"mandatory\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">validateResource</span>: <span class=\"kt\">validateResourceOfType</span><span class=\"p\">(</span><span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">s3</span><span class=\"p\">.</span><span class=\"nx\">Bucket</span><span class=\"p\">,</span> <span class=\"p\">(</span><span class=\"nx\">bucket</span><span class=\"p\">,</span> <span class=\"nx\">args</span><span class=\"p\">,</span> <span class=\"nx\">reportViolation</span><span class=\"p\">)</span> <span class=\"o\">=></span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"k\">if</span> <span class=\"p\">(</span><span class=\"nx\">bucket</span><span class=\"p\">.</span><span class=\"nx\">acl</span> <span class=\"o\">===</span> <span class=\"s2\">\"public-read\"</span> <span class=\"o\">||</span> <span class=\"nx\">bucket</span><span class=\"p\">.</span><span class=\"nx\">acl</span> <span class=\"o\">===</span> <span class=\"s2\">\"public-read-write\"</span><span class=\"p\">)</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">reportViolation</span><span class=\"p\">(</span><span class=\"s2\">\"S3 bucket must not have a public ACL.\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}),</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\n**Running a policy pack**\n\n```bash\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Scaffold a new policy pack from a template</span>\n</span></span><span class=\"line\"><span class=\"cl\">mkdir my-policies <span class=\"o\">&&</span> <span class=\"nb\">cd</span> my-policies\n</span></span><span class=\"line\"><span class=\"cl\">pulumi policy new aws-typescript\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Run policy checks against a stack</span>\n</span></span><span class=\"line\"><span class=\"cl\">pulumi preview --policy-pack ./my-policies\n</span></span><span class=\"line\"><span class=\"cl\">pulumi up --policy-pack ./my-policies\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Or publish to Pulumi Cloud and enforce organization-wide</span>\n</span></span><span class=\"line\"><span class=\"cl\">pulumi policy publish\n</span></span>\n```\n\n> As Joe Duffy, Pulumi CEO, [puts it](https://www.pulumi.com/blog/the-agentic-infrastructure-era/): “The smartest agent in the world still needs guardrails, audit trails, and policy enforcement to be trusted with production systems at scale.”\n\nPolicy checks complement unit tests: unit tests verify your intent (“I wrote tags into this resource”), while policies enforce your standards (“all resources must have these tags, regardless of what any one developer intended”). For complex compliance environments, [publish policies to Pulumi Cloud](https://www.pulumi.com/docs/insights/policy/) and assign them via policy groups to enforce them across the stacks or accounts you choose—including organization-wide.\n\n## How is testing IaC in Pulumi different from Terraform?\n\nThe fundamental difference is **language cohesion**. With Pulumi, you write infrastructure, tests, and policies all in the same general-purpose language. With Terraform, your infrastructure is HCL but your tests must be something else—either Go (Terratest) or a constrained HCL DSL (`terraform test`).\n\nPulumi\n\nTerratest\n\n`terraform test` (≥ 1.6)\n\n**Language for tests**\n\nSame language as infra (Python, TypeScript, Go, C#, Java); YAML programs tested via Automation API from any of those languages\n\nAlways Go (regardless of infra language)\n\nHCL DSL (`.tftest.hcl`)\n\n**Unit tests with mocks**\n\nYes — `pulumi.runtime.set_mocks()`, no cloud credentials needed\n\nNo — no mock layer for cloud providers\n\nLimited — `mock_provider` (v1.7+) is HCL-declarative, no programmatic logic\n\n**Integration testing**\n\nAutomation API (any language) or Go framework\n\nFull-featured but Go-only\n\nYes, via `command = apply` run blocks\n\n**Policy / guardrails**\n\nPulumi Policies: Python or TypeScript, runs at preview time\n\nExternal tools (checkov, tfsec, OPA/Rego — separate toolchain)\n\nSentinel (enterprise) or external\n\n**Execution speed**\n\nUnit: milliseconds; integration: depends on cloud\n\nMinutes (real deployments by default)\n\nPlan-only: fast; apply: minutes\n\n**Learning curve**\n\nUse existing language skills and test frameworks\n\nRequires Go knowledge and understanding of `go test`\n\nLow if you know HCL; limited expressiveness\n\nTerratest is battle-tested and widely adopted, particularly for Terraform module testing. It offers deep integrations with AWS, GCP, Azure, and Kubernetes. Its limitation is that it has no mock layer for cloud providers, so most test suites deploy real infrastructure, which makes them slower and costlier to run on every commit.\n\n`terraform test` has improved significantly in Terraform 1.6 and 1.7. The `command = plan` mode avoids real deployments, and `mock_provider` enables basic unit-like testing. But expressiveness is bounded by HCL’s declarative model—no general-purpose programming, no third-party assertion libraries, and a fixed set of `run`/`assert` test constructs.\n\nWith Pulumi, there’s no context switch. The same developer who writes the infrastructure writes the tests in the same language, using the same IDE, the same debugger, and the same CI pipeline. That cohesion is a productivity multiplier.\n\n## How to set up IaC testing with Pulumi\n\nFollow these steps to add a complete test suite to a Pulumi project. The examples use Python, but the pattern is identical in TypeScript, Go, C#, or Java.\n\n{ \"@context\": \"https://schema.org\", \"@type\": \"HowTo\", \"name\": \"How to Set Up IaC Testing with Pulumi\", \"description\": \"Add unit tests, integration tests, and policy checks to a Pulumi infrastructure project in seven steps.\", \"totalTime\": \"PT30M\", \"step\": \\[ { \"@type\": \"HowToStep\", \"name\": \"Install Pulumi and choose a language SDK\", \"text\": \"Install the Pulumi CLI and initialize a new project with your preferred language. Run 'pulumi new aws-python' (or aws-typescript, aws-go, etc.) to scaffold a project with the right SDK dependencies.\" }, { \"@type\": \"HowToStep\", \"name\": \"Add a test framework\", \"text\": \"Add a language-native test runner: pytest for Python ('pip install pytest'), Mocha + ts-node for TypeScript ('npm install --save-dev mocha ts-node @types/mocha chai'), or the standard testing package for Go (built in).\" }, { \"@type\": \"HowToStep\", \"name\": \"Implement the Mocks class and set mocks before importing your program\", \"text\": \"Create a class that extends pulumi.runtime.Mocks (Python) or implements the setMocks interface (TypeScript). Override new\\_resource to return dummy IDs and pass-through state. Call pulumi.runtime.set\\_mocks() before importing your Pulumi program—this is the critical import-order rule.\" }, { \"@type\": \"HowToStep\", \"name\": \"Write unit tests asserting resource properties\", \"text\": \"Write test functions that access stack outputs (infra.server.tags, infra.group.ingress) via pulumi.Output.all(...).apply(...). Assert security rules, required tags, naming conventions, and other properties. Decorate Python test methods with @pulumi.runtime.test; use done() callbacks in Mocha.\" }, { \"@type\": \"HowToStep\", \"name\": \"Add integration tests using the Automation API with ephemeral stacks\", \"text\": \"Import pulumi.automation (Python) or @pulumi/pulumi/automation (TypeScript). Create an ephemeral stack with create\\_or\\_select\\_stack(), call stack.up() to deploy, run assertions against stack.outputs, then call stack.destroy() in a teardown fixture. Always destroy in a finally block to avoid leaked resources.\" }, { \"@type\": \"HowToStep\", \"name\": \"Enforce guardrails with a Pulumi policy pack\", \"text\": \"Create a policy pack directory and run 'pulumi policy new aws-python' or 'pulumi policy new aws-typescript'. Define ResourceValidationPolicy rules for your compliance requirements. Use EnforcementLevel.MANDATORY to block non-compliant deployments. Run 'pulumi preview --policy-pack ./my-policies' to enforce locally, or publish to Pulumi Cloud for org-wide enforcement.\" }, { \"@type\": \"HowToStep\", \"name\": \"Run all tests in CI on every pull request\", \"text\": \"Add unit tests to your standard CI job (no cloud credentials needed). Add integration tests in a separate CI stage with cloud credentials, using a dedicated test account. Reference the Pulumi GitHub Actions integration or any CI provider—unit tests run on every PR commit; integration tests run on merge to main or on a schedule.\" } \\] }\n\n**Step 1: Install Pulumi and choose a language SDK**\n\n```bash\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Install Pulumi CLI</span>\n</span></span><span class=\"line\"><span class=\"cl\">curl -fsSL https://get.pulumi.com <span class=\"p\">|</span> sh\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Create a new project (or use an existing one)</span>\n</span></span><span class=\"line\"><span class=\"cl\">pulumi new aws-python --name my-project --stack dev\n</span></span>\n```\n\n**Step 2: Add a test framework**\n\n```bash\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Python</span>\n</span></span><span class=\"line\"><span class=\"cl\">pip install pytest pytest-asyncio\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># TypeScript</span>\n</span></span><span class=\"line\"><span class=\"cl\">npm install --save-dev mocha ts-node @types/mocha chai @types/chai\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Go uses the built-in testing package — no additional dependencies</span>\n</span></span>\n```\n\n**Step 3: Implement the Mocks class**\n\nCreate `mocks.py` (or equivalent) with your `Mocks` subclass. Keep it simple for unit tests: return `[args.name + \"_id\", args.inputs]` from `new_resource`.\n\n**Step 4: Write unit tests**\n\nCreate `test_infra.py` in your project root. Import mocks first, then your program. Write test functions that use `@pulumi.runtime.test` and `pulumi.Output.all(...).apply(...)` to assert resource properties.\n\n**Step 5: Add integration tests**\n\nCreate `test_integration.py` with a `pytest` fixture that creates a stack, calls `stack.up()`, yields outputs for assertions, and calls `stack.destroy()` in teardown. Point this at a dedicated test cloud account.\n\n**Step 6: Create a policy pack**\n\n```bash\n<span class=\"line\"><span class=\"cl\">mkdir policies <span class=\"o\">&&</span> <span class=\"nb\">cd</span> policies\n</span></span><span class=\"line\"><span class=\"cl\">pulumi policy new aws-python\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Edit __main__.py to add your rules</span>\n</span></span>\n```\n\n**Step 7: Wire it all into CI**\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"c\"># .github/workflows/test.yml (GitHub Actions excerpt)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">jobs</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">unit-tests</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">runs-on</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">ubuntu-latest</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">steps</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">uses</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">actions/checkout@v4</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">uses</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">pulumi/actions@v6</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">run</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">pip install -r requirements.txt</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">run</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">python -m pytest tests/unit/ -v</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">integration-tests</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">runs-on</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">ubuntu-latest</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">needs</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">unit-tests</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">environment</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">staging</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">steps</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">uses</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">actions/checkout@v4</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">uses</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">pulumi/actions@v6</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">run</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">pip install -r requirements.txt</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">env</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">PULUMI_ACCESS_TOKEN</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${{ secrets.PULUMI_ACCESS_TOKEN }}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">AWS_ACCESS_KEY_ID</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${{ secrets.AWS_ACCESS_KEY_ID }}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">AWS_SECRET_ACCESS_KEY</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${{ secrets.AWS_SECRET_ACCESS_KEY }}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">run</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">python -m pytest tests/integration/ -v --timeout=300</span><span class=\"w\">\n</span></span></span>\n```\n\n## Frequently asked questions\n\n### What is infrastructure as code testing?\n\nInfrastructure as code testing is the practice of writing automated checks that validate your IaC programs before they deploy to production. Just like application testing, it includes unit tests (fast, no cloud credentials, mock the provider), integration tests (real cloud deployments in ephemeral environments), and policy checks (rules that enforce compliance standards on every preview and deploy).\n\n### Can you unit test IaC without deploying to the cloud?\n\nYes. Pulumi’s mocking framework intercepts all cloud API calls and replaces them with in-process stubs. Unit tests run entirely in memory—no cloud credentials, no network calls, no cost. A suite of 50 unit tests runs in under a second. This is the primary reason to write unit tests: fast, free, deterministic feedback on every commit.\n\n### Do you need real cloud credentials to test Pulumi?\n\nOnly for integration tests. Unit tests use Pulumi’s mock runtime and require no cloud credentials at all. Policy checks also run without credentials at `pulumi preview` time. Integration tests that use the Automation API do need credentials—they deploy real resources—so they’re typically run in a CI stage with access to a dedicated test account, not on every PR.\n\n### What’s the difference between unit and integration tests for IaC?\n\nUnit tests validate resource properties, configurations, and relationships using mocked cloud providers—they run in milliseconds with no cloud credentials. Integration tests deploy actual resources to a real cloud environment, validate runtime behavior (does the endpoint respond? does the database accept connections?), and then destroy everything. Unit tests are cheap and frequent; integration tests are slower, cost real cloud spend, and run less often.\n\n### How do you mock cloud resources in Pulumi?\n\nImplement `pulumi.runtime.Mocks` (Python) or call `pulumi.runtime.setMocks()` (TypeScript/JavaScript) before importing your Pulumi program. The `new_resource` method receives every resource registration and returns a fake `[id, state]` pair. The `call` method handles data-source lookups. Set mocks with `pulumi.runtime.set_mocks(MyMocks())` and then import your program—the runtime will route all resource registrations through your mock instead of making real API calls.\n\n### What test frameworks work with Pulumi?\n\nAny language-native test framework works. For Python: **pytest** (recommended) or **unittest**. For TypeScript/JavaScript: **Mocha**, **Jest**, or **Vitest**. For Go: the standard `testing` package. For C#: **NUnit** or **xUnit**. For Java: **JUnit**. The `@pulumi/pulumi/automation` SDK and `pulumi.automation` module integrate naturally with all of them. YAML is Pulumi’s declarative configuration language; because it has no control flow or executable statements, you cannot write test logic in YAML itself. YAML programs are tested by pointing the Automation API from Python, TypeScript, Go, C#, or Java at the project directory and asserting on the stack outputs.\n\n### How do you test Pulumi in CI/CD?\n\nSplit your test suite into two CI stages. Stage 1 (unit tests): runs on every PR commit, no cloud credentials required, fast. Stage 2 (integration tests): runs on merge to main or on a schedule, requires cloud credentials for a dedicated test account, uses the Automation API to deploy ephemeral stacks. The [Pulumi GitHub Actions integration](https://github.com/pulumi/actions) simplifies credential injection and stack management in both stages.\n\n### Is policy as code the same as testing?\n\nPolicy as code is complementary to—but distinct from—testing. Tests validate what your program *produces* (resource A has property B); policies enforce what your resources *must conform to* (all resources must have tag X). Policies run automatically at preview time and block non-compliant deployments at deploy time. Together, they create defense in depth: tests verify intent, policies enforce standards regardless of intent.\n\n### How is Pulumi testing different from Terratest?\n\nTerratest typically deploys real infrastructure—it has no mock layer for cloud providers—so most test runs provision actual cloud resources, incurring real cost and taking 5–30 minutes. Tests must be written in Go regardless of what language your infrastructure uses. Pulumi’s unit testing framework runs in milliseconds with no cloud credentials, uses your existing language and test runner, and provides true mock isolation. For integration testing, Pulumi’s Automation API offers a similar model to Terratest but in any language.\n\n### How do you tear down resources after integration tests?\n\nIn Python pytest, use a module-scoped fixture that yields after `stack.up()` and calls `stack.destroy()` in the teardown. In TypeScript Mocha, call `stack.destroy()` in an `after()` hook. In Go, use `defer stack.Destroy(ctx, ...)`. Always use a `try/finally` pattern (or equivalent in your language) so teardown runs even when a test assertion fails. Pulumi Cloud also has built-in [stack TTL and auto-destroy](https://www.pulumi.com/docs/deployments/concepts/) features for extra safety.\n\n## Where to go next\n\nTesting IaC is not an all-or-nothing investment. Start by adding a handful of unit tests to your most critical stack—the one that manages your production VPC, your IAM roles, or your data infrastructure. Pick three to five properties that, if wrong, would cause an outage or a security incident. Write tests for those. Run them in CI. Expand from there.\n\nWhen you’re ready to go deeper:\n\n- **[Testing overview](https://www.pulumi.com/docs/iac/guides/testing/)** — Pulumi’s full testing documentation\n- **[Unit testing guide](https://www.pulumi.com/docs/iac/guides/testing/unit/)** — detailed mock API reference with Go and C# examples\n- **[Automation API integration testing](https://www.pulumi.com/docs/iac/guides/testing/integration/automation-api/)** — advanced patterns (multi-stack tests, config injection, parallel test execution)\n- **[Policy as code authoring](https://www.pulumi.com/docs/insights/policy/policy-packs/authoring/)** — writing and publishing Pulumi Policies\n- **[Pulumi vs. Terraform](https://www.pulumi.com/docs/iac/comparisons/terraform/)** — a full comparison of the two platforms\n\nIf you’re migrating from Terraform, the [Pulumi conversion tool](https://www.pulumi.com/tf2pulumi/) translates your existing HCL to Python, TypeScript, Go, C#, and more, including your test infrastructure. Your Terratest or `terraform test` suites can be ported to Pulumi’s native test runner—using your existing language—as part of the migration.\n\n**[Get started with Pulumi for free →](https://app.pulumi.com/signup)**","publishedAt":"2026-06-30T21:00:00.000Z","fetchedAt":"2026-07-02T17:02:05.510Z","url":"https://www.pulumi.com/blog/how-to-test-infrastructure-as-code/","media":[{"type":"image","url":"https://www.pulumi.com/blog/how-to-test-infrastructure-as-code/meta.png","r2Key":"releases/aed9f741c1e612c637db2d1fb961b9a81ddd367a6770e341e06f996b858d9bc0.png","r2Url":"https://media.releases.sh/releases/aed9f741c1e612c637db2d1fb961b9a81ddd367a6770e341e06f996b858d9bc0.png"}],"coverageCount":0},{"id":"rel_pcEG_HH32S9JlI4lNmlDS","version":null,"type":"feature","title":"Enforce ISO 27001 Across Your AWS Infrastructure","summary":"New ISO/IEC 27001:2022 policy pack for AWS, available in Pulumi Cloud as iso-27001-aws, includes 238 ready-to-run policies that map the standard's security controls to AWS resource configurations.","titleGenerated":"Pulumi ships pre-built ISO 27001 policy pack for AWS","titleShort":"Pre-built ISO 27001 policy pack for AWS","breaking":"none","importance":null,"content":"![](https://www.pulumi.com/blog/iso-27001-policy-pack-for-aws/meta.png)\n\nISO/IEC 27001 is the international standard for information security management. Proving you meet it usually means months of mapping abstract security controls to concrete cloud configuration, then authoring custom checks one resource at a time. We’re changing that.\n\nToday we’re shipping a pre-built ISO/IEC 27001:2022 policy pack for AWS, live now in Pulumi Cloud as `iso-27001-aws`. It encodes the standard’s security expectations as 238 ready-to-run policies, so you can align your AWS estate to ISO 27001 in minutes, not months.\n\n## Why ISO 27001 matters\n\nFor many companies, ISO 27001 is what stands between them and a customer or a market. The sooner you can reach a certifiable state and prove you stay there, the less compliance slows the business down. The pack collapses months of policy work into something you run continuously, so security keeps pace with growth instead of blocking it.\n\n## How the pack maps to ISO 27001\n\nThe hard part of ISO 27001 has always been translation: its controls are written in the language of governance and risk management, not in the language of AWS resources. Every team has had to interpret each control and decide what it means for an S3 bucket or an RDS instance.\n\nThe pack does that interpretation for you. Its 238 policies are aligned to the relevant ISO 27001 controls, so each result connects back to the standard instead of leaving you to map it yourself. You can browse the full pack in the [pack reference](https://www.pulumi.com/docs/reference/pre-built-policy-packs/iso-27001/aws/).\n\n## Audit and prevent\n\nThe same pack works two ways, so you can both reach compliance and stay there:\n\n1. **Audit.** Scan your existing AWS estate against the pack, including resources that Pulumi doesn’t manage. You get an honest baseline of where you stand against ISO 27001 today, with every finding tied back to the control it affects.\n2. **Prevent.** Run the same pack as a preventative policy during `pulumi up` to block non-compliant resources before they’re ever created. New infrastructure is born aligned to the standard.\n\nAudit gets you clean. Preventative policies keep you clean.\n\n## A growing library of pre-built packs\n\nISO 27001 joins a growing library of pre-built packs for AWS, each authored and maintained by Pulumi and kept current with its source standard:\n\n- **ISO/IEC 27001:2022**\n- **CIS Controls v8.1**\n- **NIST SP 800-53 Rev. 5**\n- **PCI DSS v4.0**\n- **HITRUST CSF v11.5**\n- **Pulumi Best Practices**\n\nAdopting any pack means you skip the authoring work entirely, inherit framework mappings maintained by Pulumi, and apply a consistent baseline across every stack and account.\n\n## Get started today\n\nThe ISO 27001 pack is available now to every Pulumi Cloud user:\n\n1. Browse the [pack reference](https://www.pulumi.com/docs/reference/pre-built-policy-packs/iso-27001/aws/) to see all 238 policies and how they map to the standard’s controls.\n2. Explore the full [pre-built packs index](https://www.pulumi.com/docs/insights/policy/policy-packs/pre-built-packs/).\n3. Follow the [get-started guide](https://www.pulumi.com/docs/insights/policy/get-started/) to run your first audit.\n\n## Try Pulumi policies\n\nReady to align your AWS infrastructure to ISO 27001? [Sign up for Pulumi Cloud](https://app.pulumi.com/signup) and run the pack against your estate, or read the [policy get-started guide](https://www.pulumi.com/docs/insights/policy/get-started/) to dig in.\n\nNeed a compliance pack for a framework that isn’t listed here? Open a request in [pulumi/pulumi-cloud-requests](https://github.com/pulumi/pulumi-cloud-requests) or come tell us in the [community Slack](https://slack.pulumi.com/). We’re listening.","publishedAt":"2026-06-30T00:00:00.000Z","fetchedAt":"2026-07-01T01:01:16.109Z","url":"https://www.pulumi.com/blog/iso-27001-policy-pack-for-aws/","media":[{"type":"image","url":"https://www.pulumi.com/blog/iso-27001-policy-pack-for-aws/meta.png","r2Key":"releases/92c4119386dfbf9a3d5c179af8cd773b2eb36980e122a59f89845c12d5d1a124.png","r2Url":"https://media.releases.sh/releases/92c4119386dfbf9a3d5c179af8cd773b2eb36980e122a59f89845c12d5d1a124.png"}],"coverageCount":0},{"id":"rel_xd7KcXSqiUCd3yPiGscuJ","version":null,"type":"feature","title":"Fully Automated AI Inference on AWS, Azure, and Google Cloud with Pulumi","summary":"![](https://www.pulumi.com/blog/fully-automated-ai-inference-aws-azure-gcp-pulumi/meta.png)\n\nPutting [Ollama](https://ollama.com/) on a cloud GPU is s...","titleGenerated":null,"titleShort":null,"breaking":"unknown","importance":null,"content":"![](https://www.pulumi.com/blog/fully-automated-ai-inference-aws-azure-gcp-pulumi/meta.png)\n\nPutting [Ollama](https://ollama.com/) on a cloud GPU is something I keep coming back to. A while ago I wrote up [running open-source LLMs on an AWS EC2 box with Ollama and Pulumi](https://www.pulumi.com/blog/run-deepseek-on-aws-ec2-using-pulumi/), and the shape never really changes: a GPU instance, a model server, and a firewall rule in front. Infrastructure as code earned its place by making that kind of setup predictable and repeatable, and AI infrastructure is no exception. A GPU box serving a model is still a VM, a disk, and a firewall rule, and it should be declared like one.\n\n[Thorsten Hans](https://www.linkedin.com/in/thorstenhans/) made exactly that case in his Akamai post, [Fully Automated AI Infrastructures with Terraform and Akamai Cloud](https://developers.akamai.com/blog/fully-automated-ai-infrastructure-with-terraform-and-akamai-cloud), which stands up a single GPU instance on Linode, installs the drivers, runs Ollama, and pulls a model, with no manual steps after `terraform apply`.\n\nI liked the shape of it, so this post ports the same idea to Pulumi and runs it across AWS, Azure, and Google Cloud instead of one. The result is one program shape per cloud: a single `pulumi up` brings up a GPU box that installs its own driver, runs Ollama, and pulls a model with no manual steps, and a single `pulumi destroy` takes it back down. Along the way it drops the two imperative bits the Terraform version leans on: a static access token sitting in an environment variable, and a `null_resource` running a shell loop to wait for the model. The first becomes an OIDC login from a Pulumi ESC environment, so no long-lived key lives anywhere. The second turns out not to be a resource at all.\n\n## What you are building\n\nStrip away the per-cloud naming and every version of this is the same three things: a GPU virtual machine, a firewall in front of it, and a cloud-init script that turns a bare Ubuntu box into a running inference server. The model serving runs on [Ollama](https://ollama.com/), which exposes an HTTP API on port `11434` and keeps the model resident in GPU memory between requests.\n\n```\nflowchart LR\nDev([Your machine / curl]) -->|\"HTTP :11434\"| FW[\"Firewall / security group<br/>(allow 11434, optional 22)\"]\nFW --> VM[\"GPU VM (Ubuntu 24.04)<br/>NVIDIA driver + Ollama\"]\nVM -->|\"resident in GPU memory\"| Model[\"qwen2.5:14b\"]\nESC[\"Pulumi ESC<br/>pulumi-idp/auth\"] -.->|\"OIDC login (short-lived creds)\"| Pulumi[\"pulumi up\"]\nPulumi -.->|\"AWS / Azure / GCP\"| VM\n```\n\nComponent\n\nPort\n\nDescription\n\nGPU VM\n\n\\-\n\nUbuntu 24.04 with an NVIDIA T4-class GPU. Installs the driver and Ollama from cloud-init, then pulls the model.\n\n[Ollama](https://ollama.com/)\n\n11434\n\nServes the model over an HTTP API and keeps it in GPU memory between calls.\n\nFirewall / security group\n\n\\-\n\nAllows inbound `11434` (and `22` when you ask for it). Open to the world for a demo; lock the CIDR down for anything real.\n\n`pulumi-idp/auth` (ESC)\n\n\\-\n\nOne environment that brokers an OIDC login into AWS, Azure, and GCP. No static keys in code or CI.\n\nOne detail here is worth pausing on, and it explains why the Pulumi version comes out shorter than the Terraform one. The Akamai project ends with a `null_resource` that runs a `curl` loop in `local-exec` to block `terraform apply` until the model finishes downloading. That is not infrastructure but a runtime check wearing a resource costume. Pulumi has a [Command provider](https://www.pulumi.com/registry/packages/command/) that would let you reproduce it line for line, and this post deliberately does not. The program provisions the box, the box pulls the model on its own, and the program prints the endpoint. Whether the model has finished downloading yet is a question you answer with a `curl`, not a question your IaC tool should be holding a deployment open to ask.\n\n## Prerequisites\n\nBefore getting started, ensure you have:\n\n- [Pulumi CLI](https://www.pulumi.com/docs/iac/download-install/) installed and configured\n- A [Pulumi Cloud account](https://app.pulumi.com/signup) (ESC and OIDC live here)\n- An account on at least one of [AWS](https://aws.amazon.com/), [Azure](https://azure.microsoft.com/), or [Google Cloud](https://cloud.google.com/), with an OIDC trust set up for Pulumi (covered below)\n- Enough GPU quota in your target region for one T4-class instance; a fresh account often starts at zero, which is the most common reason a first deploy fails, so check it before you run `pulumi up`\n- Node.js 18+ for the TypeScript program\n- `curl` to talk to the inference endpoint once it comes up\n\nThis post serves the `qwen2.5:14b` model, the same one the Akamai article uses, because the 4-bit quant fits comfortably on a single 16 GB T4. The model is one config value (`model`), so swap in anything [Ollama supports](https://ollama.com/library); match the GPU to the model’s memory footprint, because a model that overflows VRAM still runs but spills onto the CPU and crawls.\n\n## Credentials without the copy-paste\n\nThe Terraform version authenticates the only way a single-cloud demo can: you mint a personal access token, export it as `LINODE_TOKEN`, and the provider reads it from the environment. It works, but that token is long-lived, it sits in your shell history and your CI secrets, and you mint one per cloud. Across three clouds that is three static keys to rotate and worry about.\n\nPulumi [ESC (Environments, Secrets, and Configuration)](https://www.pulumi.com/docs/esc/) replaces it all with an OIDC login. The idea: instead of storing a cloud key, you store a *trust relationship*. At deploy time, ESC presents a short-lived OIDC token to AWS, Azure, or Google Cloud, and each one hands back temporary credentials scoped to a role you control. Nothing long-lived is ever written down.\n\nI keep that wiring in one environment, `pulumi-idp/auth`, and every stack imports it. Here is the whole thing:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"c\"># pulumi-idp/auth: one ESC environment that brokers an OIDC login into all three</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c\"># clouds. Every stack imports it. No static cloud key lives here or anywhere else.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">values</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">aws</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">login</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">fn::open::aws-login</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">oidc</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">roleArn</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">arn:aws:iam::123456789012:role/pulumi-esc</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">sessionName</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">pulumi-esc</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">azure</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">login</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">fn::open::azure-login</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">clientId</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">tenantId</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">subscriptionId</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">/subscriptions/00000000-0000-0000-0000-000000000000</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">oidc</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kc\">true</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">gcp</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">login</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">fn::open::gcp-login</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">project</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">123456789012</span><span class=\"w\"> </span><span class=\"c\"># numeric project number, not the project ID</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">oidc</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">workloadPoolId</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">pulumi-esc</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">providerId</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">pulumi-esc</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">serviceAccount</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">pulumi-esc@my-project.iam.gserviceaccount.com</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">environmentVariables</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># AWS: read by the Pulumi AWS provider, the AWS SDKs, and the aws CLI</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">AWS_ACCESS_KEY_ID</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${aws.login.accessKeyId}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">AWS_SECRET_ACCESS_KEY</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${aws.login.secretAccessKey}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">AWS_SESSION_TOKEN</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${aws.login.sessionToken}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Azure: read by the azure-native provider</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ARM_USE_OIDC</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"true\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ARM_CLIENT_ID</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${azure.login.clientId}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ARM_TENANT_ID</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${azure.login.tenantId}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ARM_SUBSCRIPTION_ID</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${azure.login.subscriptionId}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ARM_OIDC_TOKEN</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${azure.login.oidc.token}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Google Cloud: read by the Pulumi Google Cloud provider</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">GOOGLE_CLOUD_PROJECT</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${gcp.login.project}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">GOOGLE_OAUTH_ACCESS_TOKEN</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${gcp.login.accessToken}</span><span class=\"w\">\n</span></span></span>\n```\n\nYou only need the blocks for the clouds you actually deploy to. Opening this environment makes ESC perform a live OIDC login for each cloud listed, so trim it to the one you use, or keep all three if, like me, you bounce between them.\n\nEach cloud needs a one-time trust setup so this login is allowed at all: an [IAM OIDC identity provider and role on AWS](https://www.pulumi.com/docs/esc/guides/configuring-oidc/aws/), a [federated credential on an Azure app registration](https://www.pulumi.com/docs/esc/guides/configuring-oidc/azure/), and a [Workload Identity Pool on Google Cloud](https://www.pulumi.com/docs/esc/guides/configuring-oidc/gcp/). You do that once; after that, `pulumi-idp/auth` is the only thing any stack references for credentials.\n\nA stack opts into it with one block in its stack config. The AWS stack’s `Pulumi.aws.yaml`:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"c\"># Pulumi.aws.yaml</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">environment</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"l\">pulumi-idp/auth</span><span class=\"w\">\n</span></span></span>\n```\n\nThat is the entire credential story. No keys in the program, no keys in CI, nothing to rotate. The program below never mentions a secret; it only creates resources, and the ambient credentials from `pulumi-idp/auth` carry the request.\n\n## One cloud-init, three clouds\n\nThe part that turns a bare Ubuntu box into an inference server is identical on every cloud, so it lives in one file, `cloud-init.yaml`, that all three programs read. It follows the Akamai cloud-config closely, with a couple of robustness tweaks for a multi-cloud run, and it runs in a deliberate order because the GPU driver needs a reboot before Ollama can see the card:\n\n1. Update packages and install the kernel headers and `ubuntu-drivers`.\n2. Install the NVIDIA driver, then reboot so the kernel loads it.\n3. On the next boot, a one-shot systemd service installs Ollama, binds it to `0.0.0.0:11434`, and pulls the model.\n4. The service disables itself, so it never runs again.\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"c\">#cloud-config</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c\"># Zero-touch Ollama GPU box. Cloud-agnostic: no provider metadata, no static creds.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c\"># The Pulumi program substitutes the model name on the `ollama pull` line below</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c\"># before this is passed as user-data (AWS/GCP) or custom-data (Azure).</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">write_files</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Make Ollama listen on every interface and never unload the model from VRAM.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">path</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">/etc/systemd/system/ollama.service.d/override.conf</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">content</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"p\">|</span><span class=\"sd\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> [Service]\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> Environment=\"OLLAMA_HOST=0.0.0.0:11434\"\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> Environment=\"OLLAMA_KEEP_ALIVE=-1\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Runs once on the post-reboot boot, after the GPU driver is loaded.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">path</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">/usr/local/bin/ollama-setup.sh</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">permissions</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s1\">'0755'</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">content</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"p\">|</span><span class=\"sd\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> #!/usr/bin/env bash\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> set -euxo pipefail\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> # Install Ollama; the installer creates and starts the ollama systemd unit.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> curl -fsSL https://ollama.com/install.sh | sh\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> # Pick up the OLLAMA_HOST / OLLAMA_KEEP_ALIVE override written above.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> systemctl daemon-reload\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> systemctl enable ollama.service\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> systemctl restart ollama.service\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> # Wait for the daemon to bind its socket before pulling. This is the box\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> # waiting on its OWN local daemon, not an external readiness gate.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> until curl -fsS http://127.0.0.1:11434/api/tags >/dev/null 2>&1; do\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> sleep 2\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> done\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> # Pre-pull the model so the endpoint answers on the very first request.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> ollama pull __MODEL__\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> # One-shot: never run again on future boots.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> systemctl disable ollama-setup.service</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">path</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">/etc/systemd/system/ollama-setup.service</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">content</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"p\">|</span><span class=\"sd\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> [Unit]\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> Description=One-time Ollama install and model pull\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> After=network-online.target\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> Wants=network-online.target\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> [Service]\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> Type=oneshot\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> ExecStart=/usr/local/bin/ollama-setup.sh\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> RemainAfterExit=true\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> # The model pull runs for several minutes; without this, systemd's default\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> # 90s start timeout kills the service mid-download and the model never lands.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> TimeoutStartSec=0\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> [Install]\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sd\"> WantedBy=multi-user.target</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">runcmd</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"l\">apt-get update</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"l\">DEBIAN_FRONTEND=noninteractive apt-get -y upgrade</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># linux-headers-generic also covers the kernel the upgrade above may have pulled</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># in, so DKMS builds the NVIDIA module for the kernel that boots next.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"l\">DEBIAN_FRONTEND=noninteractive apt-get install -y \"linux-headers-$(uname -r)\" linux-headers-generic ubuntu-drivers-common</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># --gpgpu selects the headless server driver branch, the right one for a compute</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># GPU like the T4; bare `ubuntu-drivers install` would pull the desktop stack.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"l\">ubuntu-drivers install --gpgpu</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"l\">systemctl enable ollama-setup.service</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">power_state</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">mode</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">reboot</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">message</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">Rebooting to load the NVIDIA driver before Ollama setup</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">condition</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kc\">true</span><span class=\"w\">\n</span></span></span>\n```\n\nThe model name is the literal token `__MODEL__`. The Pulumi program reads this file at deploy time and substitutes your `model` config value before handing it to the instance. Two environment settings are doing quiet but important work: `OLLAMA_HOST=0.0.0.0:11434` makes Ollama listen on every interface instead of localhost alone, and `OLLAMA_KEEP_ALIVE=-1` keeps the model pinned in GPU memory so only the first request pays the load cost.\n\n## The programs\n\nEvery program has the same five beats: read the config, template the `cloud-init.yaml`, open the inbound ports, launch the GPU instance with that cloud-init as its user data, and export the endpoint. What differs is only the dialect each cloud speaks for “GPU instance” and “firewall rule.”\n\nThe shared contract keeps the three listings legible side by side: the same `model` and `allowSsh` config keys, the same `cloud-init.yaml`, and the same four exports (`publicIp`, `ollamaEndpoint`, `generateEndpoint`, `tagsEndpoint`). Pick your cloud:\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">pulumi</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/pulumi\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">aws</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/aws\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">fs</span> <span class=\"kr\">from</span> <span class=\"s2\">\"fs\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">cfg</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">Config</span><span class=\"p\">();</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">model</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"kr\">get</span><span class=\"p\">(</span><span class=\"s2\">\"model\"</span><span class=\"p\">)</span> <span class=\"o\">??</span> <span class=\"s2\">\"qwen2.5:14b\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">allowSsh</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"nx\">getBoolean</span><span class=\"p\">(</span><span class=\"s2\">\"allowSsh\"</span><span class=\"p\">)</span> <span class=\"o\">??</span> <span class=\"kc\">false</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">region</span> <span class=\"o\">=</span> <span class=\"p\">(</span><span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"kr\">get</span><span class=\"p\">(</span><span class=\"s2\">\"region\"</span><span class=\"p\">)</span> <span class=\"o\">??</span> <span class=\"s2\">\"us-east-1\"</span><span class=\"p\">)</span> <span class=\"kr\">as</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">Region</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Inject the model name into cloud-init at deploy time (a file read, not a shell-out).\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">userData</span> <span class=\"o\">=</span> <span class=\"nx\">fs</span><span class=\"p\">.</span><span class=\"nx\">readFileSync</span><span class=\"p\">(</span><span class=\"s2\">\"cloud-init.yaml\"</span><span class=\"p\">,</span> <span class=\"s2\">\"utf8\"</span><span class=\"p\">).</span><span class=\"nx\">replace</span><span class=\"p\">(</span><span class=\"sr\">/__MODEL__/g</span><span class=\"p\">,</span> <span class=\"nx\">model</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Region comes from config; credentials arrive ambiently from pulumi-idp/auth.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">provider</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">Provider</span><span class=\"p\">(</span><span class=\"s2\">\"aws\"</span><span class=\"p\">,</span> <span class=\"p\">{</span> <span class=\"nx\">region</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Most recent Ubuntu 24.04 LTS (amd64, hvm, gp3) published by Canonical.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">ubuntu</span> <span class=\"o\">=</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">getAmi</span><span class=\"p\">({</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">mostRecent</span>: <span class=\"kt\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">owners</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"099720109477\"</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">filters</span><span class=\"o\">:</span> <span class=\"p\">[</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"name\"</span><span class=\"p\">,</span> <span class=\"nx\">values</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*\"</span><span class=\"p\">]</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"virtualization-type\"</span><span class=\"p\">,</span> <span class=\"nx\">values</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"hvm\"</span><span class=\"p\">]</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">},</span> <span class=\"p\">{</span> <span class=\"nx\">provider</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">ingress</span>: <span class=\"kt\">aws.types.input.ec2.SecurityGroupIngress</span><span class=\"p\">[]</span> <span class=\"o\">=</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">description</span><span class=\"o\">:</span> <span class=\"s2\">\"Ollama HTTP API. NOTE: restrict cidrBlocks to your own range in production.\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">fromPort</span>: <span class=\"kt\">11434</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">toPort</span>: <span class=\"kt\">11434</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">protocol</span><span class=\"o\">:</span> <span class=\"s2\">\"tcp\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">cidrBlocks</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"0.0.0.0/0\"</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}];</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">if</span> <span class=\"p\">(</span><span class=\"nx\">allowSsh</span><span class=\"p\">)</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ingress</span><span class=\"p\">.</span><span class=\"nx\">push</span><span class=\"p\">({</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">description</span><span class=\"o\">:</span> <span class=\"s2\">\"SSH\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">fromPort</span>: <span class=\"kt\">22</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">toPort</span>: <span class=\"kt\">22</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">protocol</span><span class=\"o\">:</span> <span class=\"s2\">\"tcp\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">cidrBlocks</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"0.0.0.0/0\"</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">sg</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">SecurityGroup</span><span class=\"p\">(</span><span class=\"s2\">\"ollama\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">description</span><span class=\"o\">:</span> <span class=\"s2\">\"Ollama inferencing access\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ingress</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">egress</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">description</span><span class=\"o\">:</span> <span class=\"s2\">\"Allow all outbound\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">fromPort</span>: <span class=\"kt\">0</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">toPort</span>: <span class=\"kt\">0</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">protocol</span><span class=\"o\">:</span> <span class=\"s2\">\"-1\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">cidrBlocks</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"0.0.0.0/0\"</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">},</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">provider</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">server</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">aws</span><span class=\"p\">.</span><span class=\"nx\">ec2</span><span class=\"p\">.</span><span class=\"nx\">Instance</span><span class=\"p\">(</span><span class=\"s2\">\"ollama\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ami</span>: <span class=\"kt\">ubuntu.then</span><span class=\"p\">(</span><span class=\"nx\">a</span> <span class=\"o\">=></span> <span class=\"nx\">a</span><span class=\"p\">.</span><span class=\"nx\">id</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">instanceType</span><span class=\"o\">:</span> <span class=\"s2\">\"g4dn.xlarge\"</span><span class=\"p\">,</span> <span class=\"c1\">// 1x NVIDIA T4\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">vpcSecurityGroupIds</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"nx\">sg</span><span class=\"p\">.</span><span class=\"nx\">id</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">associatePublicIpAddress</span>: <span class=\"kt\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">userData</span><span class=\"p\">,</span> <span class=\"c1\">// plain text; the AWS provider base64-encodes it for you\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">rootBlockDevice</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">volumeSize</span>: <span class=\"kt\">40</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">volumeType</span><span class=\"o\">:</span> <span class=\"s2\">\"gp3\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">tags</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">Name</span><span class=\"o\">:</span> <span class=\"s2\">\"ollama\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">},</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">provider</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">publicIp</span> <span class=\"o\">=</span> <span class=\"nx\">server</span><span class=\"p\">.</span><span class=\"nx\">publicIp</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">ollamaEndpoint</span> <span class=\"o\">=</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">interpolate</span><span class=\"sb\">`http://</span><span class=\"si\">${</span><span class=\"nx\">publicIp</span><span class=\"si\">}</span><span class=\"sb\">:11434`</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">generateEndpoint</span> <span class=\"o\">=</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">interpolate</span><span class=\"sb\">`http://</span><span class=\"si\">${</span><span class=\"nx\">publicIp</span><span class=\"si\">}</span><span class=\"sb\">:11434/api/generate`</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">tagsEndpoint</span> <span class=\"o\">=</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">interpolate</span><span class=\"sb\">`http://</span><span class=\"si\">${</span><span class=\"nx\">publicIp</span><span class=\"si\">}</span><span class=\"sb\">:11434/api/tags`</span><span class=\"p\">;</span>\n</span></span>\n```\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">pulumi</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/pulumi\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">gcp</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/gcp\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">fs</span> <span class=\"kr\">from</span> <span class=\"s2\">\"fs\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">cfg</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">Config</span><span class=\"p\">();</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">model</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"kr\">get</span><span class=\"p\">(</span><span class=\"s2\">\"model\"</span><span class=\"p\">)</span> <span class=\"o\">??</span> <span class=\"s2\">\"qwen2.5:14b\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">allowSsh</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"nx\">getBoolean</span><span class=\"p\">(</span><span class=\"s2\">\"allowSsh\"</span><span class=\"p\">)</span> <span class=\"o\">??</span> <span class=\"kc\">false</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">project</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"kr\">get</span><span class=\"p\">(</span><span class=\"s2\">\"project\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">zone</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"kr\">get</span><span class=\"p\">(</span><span class=\"s2\">\"zone\"</span><span class=\"p\">)</span> <span class=\"o\">??</span> <span class=\"s2\">\"us-central1-a\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Inject the model name into cloud-init at deploy time (a file read, not a shell-out).\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">userData</span> <span class=\"o\">=</span> <span class=\"nx\">fs</span><span class=\"p\">.</span><span class=\"nx\">readFileSync</span><span class=\"p\">(</span><span class=\"s2\">\"cloud-init.yaml\"</span><span class=\"p\">,</span> <span class=\"s2\">\"utf8\"</span><span class=\"p\">).</span><span class=\"nx\">replace</span><span class=\"p\">(</span><span class=\"sr\">/__MODEL__/g</span><span class=\"p\">,</span> <span class=\"nx\">model</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Latest Ubuntu 24.04 LTS amd64 image.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">ubuntu</span> <span class=\"o\">=</span> <span class=\"nx\">gcp</span><span class=\"p\">.</span><span class=\"nx\">compute</span><span class=\"p\">.</span><span class=\"nx\">getImage</span><span class=\"p\">({</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">family</span><span class=\"o\">:</span> <span class=\"s2\">\"ubuntu-2404-lts-amd64\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">project</span><span class=\"o\">:</span> <span class=\"s2\">\"ubuntu-os-cloud\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Network tag binds the firewall rule to this instance.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">networkTag</span> <span class=\"o\">=</span> <span class=\"s2\">\"ollama\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">firewall</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">gcp</span><span class=\"p\">.</span><span class=\"nx\">compute</span><span class=\"p\">.</span><span class=\"nx\">Firewall</span><span class=\"p\">(</span><span class=\"s2\">\"ollama-fw\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">network</span><span class=\"o\">:</span> <span class=\"s2\">\"default\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">project</span>: <span class=\"kt\">project</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Inbound to Ollama from anywhere; restrict this CIDR for production.\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">allows</span>: <span class=\"kt\">allowSsh</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"o\">?</span> <span class=\"p\">[{</span> <span class=\"nx\">protocol</span><span class=\"o\">:</span> <span class=\"s2\">\"tcp\"</span><span class=\"p\">,</span> <span class=\"nx\">ports</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"11434\"</span><span class=\"p\">]</span> <span class=\"p\">},</span> <span class=\"p\">{</span> <span class=\"nx\">protocol</span><span class=\"o\">:</span> <span class=\"s2\">\"tcp\"</span><span class=\"p\">,</span> <span class=\"nx\">ports</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"22\"</span><span class=\"p\">]</span> <span class=\"p\">}]</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"o\">:</span> <span class=\"p\">[{</span> <span class=\"nx\">protocol</span><span class=\"o\">:</span> <span class=\"s2\">\"tcp\"</span><span class=\"p\">,</span> <span class=\"nx\">ports</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"11434\"</span><span class=\"p\">]</span> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">sourceRanges</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"0.0.0.0/0\"</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">targetTags</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"nx\">networkTag</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">instance</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">gcp</span><span class=\"p\">.</span><span class=\"nx\">compute</span><span class=\"p\">.</span><span class=\"nx\">Instance</span><span class=\"p\">(</span><span class=\"s2\">\"ollama\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">machineType</span><span class=\"o\">:</span> <span class=\"s2\">\"n1-standard-4\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">zone</span>: <span class=\"kt\">zone</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">project</span>: <span class=\"kt\">project</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">tags</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"nx\">networkTag</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">bootDisk</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">initializeParams</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">image</span>: <span class=\"kt\">ubuntu.then</span><span class=\"p\">(</span><span class=\"nx\">i</span> <span class=\"o\">=></span> <span class=\"nx\">i</span><span class=\"p\">.</span><span class=\"nx\">selfLink</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">size</span>: <span class=\"kt\">40</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">guestAccelerators</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">type</span><span class=\"o\">:</span> <span class=\"s2\">\"nvidia-tesla-t4\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">count</span>: <span class=\"kt\">1</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// GPUs cannot live-migrate, so host maintenance must terminate (and restart) the VM.\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">scheduling</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">onHostMaintenance</span><span class=\"o\">:</span> <span class=\"s2\">\"TERMINATE\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">automaticRestart</span>: <span class=\"kt\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">networkInterfaces</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">network</span><span class=\"o\">:</span> <span class=\"s2\">\"default\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">accessConfigs</span><span class=\"o\">:</span> <span class=\"p\">[{}],</span> <span class=\"c1\">// an empty config requests an ephemeral public IP\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">metadata</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"user-data\"</span><span class=\"o\">:</span> <span class=\"nx\">userData</span><span class=\"p\">,</span> <span class=\"c1\">// Ubuntu cloud-init reads this key, not startup-script\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">},</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">dependsOn</span>: <span class=\"kt\">firewall</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">publicIp</span> <span class=\"o\">=</span> <span class=\"nx\">instance</span><span class=\"p\">.</span><span class=\"nx\">networkInterfaces</span><span class=\"p\">.</span><span class=\"nx\">apply</span><span class=\"p\">(</span><span class=\"nx\">nics</span> <span class=\"o\">=></span> <span class=\"nx\">nics</span><span class=\"p\">[</span><span class=\"mi\">0</span><span class=\"p\">].</span><span class=\"nx\">accessConfigs</span><span class=\"o\">!</span><span class=\"p\">[</span><span class=\"mi\">0</span><span class=\"p\">].</span><span class=\"nx\">natIp</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">ollamaEndpoint</span> <span class=\"o\">=</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">interpolate</span><span class=\"sb\">`http://</span><span class=\"si\">${</span><span class=\"nx\">publicIp</span><span class=\"si\">}</span><span class=\"sb\">:11434`</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">generateEndpoint</span> <span class=\"o\">=</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">interpolate</span><span class=\"sb\">`http://</span><span class=\"si\">${</span><span class=\"nx\">publicIp</span><span class=\"si\">}</span><span class=\"sb\">:11434/api/generate`</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">tagsEndpoint</span> <span class=\"o\">=</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">interpolate</span><span class=\"sb\">`http://</span><span class=\"si\">${</span><span class=\"nx\">publicIp</span><span class=\"si\">}</span><span class=\"sb\">:11434/api/tags`</span><span class=\"p\">;</span>\n</span></span>\n```\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">pulumi</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/pulumi\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">resources</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/azure-native/resources\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">network</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/azure-native/network\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">compute</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/azure-native/compute\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">random</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/random\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">fs</span> <span class=\"kr\">from</span> <span class=\"s2\">\"fs\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">cfg</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">Config</span><span class=\"p\">();</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">model</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"kr\">get</span><span class=\"p\">(</span><span class=\"s2\">\"model\"</span><span class=\"p\">)</span> <span class=\"o\">??</span> <span class=\"s2\">\"qwen2.5:14b\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">allowSsh</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"nx\">getBoolean</span><span class=\"p\">(</span><span class=\"s2\">\"allowSsh\"</span><span class=\"p\">)</span> <span class=\"o\">??</span> <span class=\"kc\">false</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">location</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"kr\">get</span><span class=\"p\">(</span><span class=\"s2\">\"location\"</span><span class=\"p\">)</span> <span class=\"o\">??</span> <span class=\"s2\">\"eastus\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Inject the model name into cloud-init at deploy time (a file read, not a shell-out).\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">userData</span> <span class=\"o\">=</span> <span class=\"nx\">fs</span><span class=\"p\">.</span><span class=\"nx\">readFileSync</span><span class=\"p\">(</span><span class=\"s2\">\"cloud-init.yaml\"</span><span class=\"p\">,</span> <span class=\"s2\">\"utf8\"</span><span class=\"p\">).</span><span class=\"nx\">replace</span><span class=\"p\">(</span><span class=\"sr\">/__MODEL__/g</span><span class=\"p\">,</span> <span class=\"nx\">model</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">resourceGroup</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">resources</span><span class=\"p\">.</span><span class=\"nx\">ResourceGroup</span><span class=\"p\">(</span><span class=\"s2\">\"ollama-rg\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">location</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">vnet</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">network</span><span class=\"p\">.</span><span class=\"nx\">VirtualNetwork</span><span class=\"p\">(</span><span class=\"s2\">\"ollama-vnet\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">resourceGroupName</span>: <span class=\"kt\">resourceGroup.name</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">addressSpace</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">addressPrefixes</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"10.0.0.0/16\"</span><span class=\"p\">]</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">subnet</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">network</span><span class=\"p\">.</span><span class=\"nx\">Subnet</span><span class=\"p\">(</span><span class=\"s2\">\"ollama-subnet\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">resourceGroupName</span>: <span class=\"kt\">resourceGroup.name</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">virtualNetworkName</span>: <span class=\"kt\">vnet.name</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">addressPrefix</span><span class=\"o\">:</span> <span class=\"s2\">\"10.0.1.0/24\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Standard SKU public IPs use static allocation.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">publicIpAddress</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">network</span><span class=\"p\">.</span><span class=\"nx\">PublicIPAddress</span><span class=\"p\">(</span><span class=\"s2\">\"ollama-pip\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">resourceGroupName</span>: <span class=\"kt\">resourceGroup.name</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">sku</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">name</span>: <span class=\"kt\">network.PublicIPAddressSkuName.Standard</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">publicIPAllocationMethod</span>: <span class=\"kt\">network.IPAllocationMethod.Static</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Azure permits all outbound by default, so only inbound rules are needed.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">nsg</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">network</span><span class=\"p\">.</span><span class=\"nx\">NetworkSecurityGroup</span><span class=\"p\">(</span><span class=\"s2\">\"ollama-nsg\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">resourceGroupName</span>: <span class=\"kt\">resourceGroup.name</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">securityRules</span><span class=\"o\">:</span> <span class=\"p\">[</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"allow-ollama\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">priority</span>: <span class=\"kt\">1000</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">direction</span>: <span class=\"kt\">network.SecurityRuleDirection.Inbound</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">access</span>: <span class=\"kt\">network.SecurityRuleAccess.Allow</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">protocol</span>: <span class=\"kt\">network.SecurityRuleProtocol.Tcp</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">sourcePortRange</span><span class=\"o\">:</span> <span class=\"s2\">\"*\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">destinationPortRange</span><span class=\"o\">:</span> <span class=\"s2\">\"11434\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">sourceAddressPrefix</span><span class=\"o\">:</span> <span class=\"s2\">\"0.0.0.0/0\"</span><span class=\"p\">,</span> <span class=\"c1\">// prod: restrict to your client CIDR\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">destinationAddressPrefix</span><span class=\"o\">:</span> <span class=\"s2\">\"*\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">...(</span><span class=\"nx\">allowSsh</span> <span class=\"o\">?</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"allow-ssh\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">priority</span>: <span class=\"kt\">1001</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">direction</span>: <span class=\"kt\">network.SecurityRuleDirection.Inbound</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">access</span>: <span class=\"kt\">network.SecurityRuleAccess.Allow</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">protocol</span>: <span class=\"kt\">network.SecurityRuleProtocol.Tcp</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">sourcePortRange</span><span class=\"o\">:</span> <span class=\"s2\">\"*\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">destinationPortRange</span><span class=\"o\">:</span> <span class=\"s2\">\"22\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">sourceAddressPrefix</span><span class=\"o\">:</span> <span class=\"s2\">\"0.0.0.0/0\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">destinationAddressPrefix</span><span class=\"o\">:</span> <span class=\"s2\">\"*\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}]</span> <span class=\"o\">:</span> <span class=\"p\">[]),</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">nic</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">network</span><span class=\"p\">.</span><span class=\"nx\">NetworkInterface</span><span class=\"p\">(</span><span class=\"s2\">\"ollama-nic\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">resourceGroupName</span>: <span class=\"kt\">resourceGroup.name</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">networkSecurityGroup</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">id</span>: <span class=\"kt\">nsg.id</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ipConfigurations</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"ipconfig1\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">subnet</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">id</span>: <span class=\"kt\">subnet.id</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">publicIPAddress</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">id</span>: <span class=\"kt\">publicIpAddress.id</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">privateIPAllocationMethod</span>: <span class=\"kt\">network.IPAllocationMethod.Dynamic</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">primary</span>: <span class=\"kt\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Azure requires an admin credential even though we never log in. Generate one\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// instead of hard-coding it; it stays a Pulumi secret and is never exported.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">adminPassword</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">random</span><span class=\"p\">.</span><span class=\"nx\">RandomPassword</span><span class=\"p\">(</span><span class=\"s2\">\"ollama-admin-password\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">length</span>: <span class=\"kt\">24</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">special</span>: <span class=\"kt\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">overrideSpecial</span><span class=\"o\">:</span> <span class=\"s2\">\"!#$%*\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">minLower</span>: <span class=\"kt\">1</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">minUpper</span>: <span class=\"kt\">1</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">minNumeric</span>: <span class=\"kt\">1</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">minSpecial</span>: <span class=\"kt\">1</span><span class=\"p\">,</span> <span class=\"c1\">// Azure requires 3 of 4 character classes\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">vm</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">compute</span><span class=\"p\">.</span><span class=\"nx\">VirtualMachine</span><span class=\"p\">(</span><span class=\"s2\">\"ollama-vm\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">resourceGroupName</span>: <span class=\"kt\">resourceGroup.name</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">hardwareProfile</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">vmSize</span><span class=\"o\">:</span> <span class=\"s2\">\"Standard_NC4as_T4_v3\"</span> <span class=\"p\">},</span> <span class=\"c1\">// 1x NVIDIA T4\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">networkProfile</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">networkInterfaces</span><span class=\"o\">:</span> <span class=\"p\">[{</span> <span class=\"nx\">id</span>: <span class=\"kt\">nic.id</span><span class=\"p\">,</span> <span class=\"nx\">primary</span>: <span class=\"kt\">true</span> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">osProfile</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">computerName</span><span class=\"o\">:</span> <span class=\"s2\">\"ollama\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">adminUsername</span><span class=\"o\">:</span> <span class=\"s2\">\"azureuser\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">adminPassword</span>: <span class=\"kt\">adminPassword.result</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">customData</span>: <span class=\"kt\">Buffer.from</span><span class=\"p\">(</span><span class=\"nx\">userData</span><span class=\"p\">).</span><span class=\"nx\">toString</span><span class=\"p\">(</span><span class=\"s2\">\"base64\"</span><span class=\"p\">),</span> <span class=\"c1\">// Azure wants base64\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">linuxConfiguration</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">disablePasswordAuthentication</span>: <span class=\"kt\">false</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">storageProfile</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">imageReference</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">publisher</span><span class=\"o\">:</span> <span class=\"s2\">\"Canonical\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">offer</span><span class=\"o\">:</span> <span class=\"s2\">\"ubuntu-24_04-lts\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">sku</span><span class=\"o\">:</span> <span class=\"s2\">\"server\"</span><span class=\"p\">,</span> <span class=\"c1\">// Gen2; NC4as_T4_v3 is a Gen2 size\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">version</span><span class=\"o\">:</span> <span class=\"s2\">\"latest\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">osDisk</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"ollama-osdisk\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">createOption</span><span class=\"o\">:</span> <span class=\"s2\">\"FromImage\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">diskSizeGB</span>: <span class=\"kt\">40</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">managedDisk</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">storageAccountType</span><span class=\"o\">:</span> <span class=\"s2\">\"StandardSSD_LRS\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// A Standard/Static public IP is reserved at creation, so its address is known\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// once the resource exists; no separate lookup needed.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">publicIp</span> <span class=\"o\">=</span> <span class=\"nx\">publicIpAddress</span><span class=\"p\">.</span><span class=\"nx\">ipAddress</span><span class=\"p\">.</span><span class=\"nx\">apply</span><span class=\"p\">(</span><span class=\"nx\">ip</span> <span class=\"o\">=></span> <span class=\"nx\">ip</span><span class=\"o\">!</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">ollamaEndpoint</span> <span class=\"o\">=</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">interpolate</span><span class=\"sb\">`http://</span><span class=\"si\">${</span><span class=\"nx\">publicIp</span><span class=\"si\">}</span><span class=\"sb\">:11434`</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">generateEndpoint</span> <span class=\"o\">=</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">interpolate</span><span class=\"sb\">`http://</span><span class=\"si\">${</span><span class=\"nx\">publicIp</span><span class=\"si\">}</span><span class=\"sb\">:11434/api/generate`</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">tagsEndpoint</span> <span class=\"o\">=</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">interpolate</span><span class=\"sb\">`http://</span><span class=\"si\">${</span><span class=\"nx\">publicIp</span><span class=\"si\">}</span><span class=\"sb\">:11434/api/tags`</span><span class=\"p\">;</span>\n</span></span>\n```\n\nA few per-cloud details are worth calling out, since they are the places the “same program” abstraction leaks:\n\n- **AWS** is the shortest program, because the GPU comes with the instance shape: a `g4dn.xlarge` *is* a T4 box, so there is no separate accelerator to attach. The one thing to do before you deploy is raise the **Running On-Demand G and VT instances** vCPU quota in your region; a fresh account starts at zero, and `pulumi up` fails with `VcpuLimitExceeded` until you do.\n- **Google Cloud** attaches the GPU explicitly with `guestAccelerators`, and that brings the rule that trips people up: a GPU instance cannot live-migrate, so `scheduling.onHostMaintenance` must be `\"TERMINATE\"` or the apply is rejected. The cloud-init also has to ride on the `user-data` metadata key, not `startup-script`, and the empty `accessConfigs: [{}]` is what hands the box a public IP. T4 quota is also zero on a new project.\n- **Azure** is the longest listing, because the network is à la carte: the resource group, virtual network, subnet, public IP, security group, and NIC are each their own resource before you reach the VM. One detail surprises people: a Linux VM requires an admin credential even when you never log in, so the program generates a throwaway password with `random.RandomPassword` rather than committing one. The `NC4as_T4_v3` is a compute GPU, so the standard server (CUDA) driver from the cloud-init is the right choice here; the GRID driver is only needed for GPU-accelerated visualization workloads.\n\nThe full programs, all three `Pulumi.<cloud>.yaml` files, and the shared `cloud-init.yaml` are in the companion repo:\n\n[![GitHub repository: dirien/fully-automated-ai-inference-pulumi](https://opengraph.githubassets.com/1/dirien/fully-automated-ai-inference-pulumi)\n\ngithub.com/dirien/fully-automated-ai-inference-pulumi\n\n](https://github.com/dirien/fully-automated-ai-inference-pulumi)\n\n## Deploying\n\nCreate a project, install the provider for your cloud, point the stack at `pulumi-idp/auth`, and deploy. For AWS:\n\n```bash\n<span class=\"line\"><span class=\"cl\">mkdir ai-inference <span class=\"o\">&&</span> <span class=\"nb\">cd</span> ai-inference\n</span></span><span class=\"line\"><span class=\"cl\">pulumi new typescript\n</span></span><span class=\"line\"><span class=\"cl\">npm install @pulumi/aws\n</span></span>\n```\n\nDrop the AWS listing into `index.ts`, copy the `cloud-init.yaml` shown earlier into the same directory (or grab it from the companion repo), point the stack at the auth environment, and set your region:\n\n```bash\n<span class=\"line\"><span class=\"cl\">pulumi stack init aws\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># add `environment: [pulumi-idp/auth]` to Pulumi.aws.yaml (shown above)</span>\n</span></span><span class=\"line\"><span class=\"cl\">pulumi config <span class=\"nb\">set</span> region us-east-1\n</span></span><span class=\"line\"><span class=\"cl\">pulumi config <span class=\"nb\">set</span> model qwen2.5:14b\n</span></span><span class=\"line\"><span class=\"cl\">pulumi up\n</span></span>\n```\n\nThe other two clouds are the same flow with a different provider package and a couple of config keys: `npm install @pulumi/gcp` with `pulumi config set project <your-project-id>` and `pulumi config set zone us-central1-a`, or `npm install @pulumi/azure-native @pulumi/random` with `pulumi config set location eastus`.\n\nThe slow part is the GPU box: it boots, installs the driver, reboots, installs Ollama, and pulls the model, all without you. Depending on the instance, the region, and the model size, plan on roughly 10 to 15 minutes before the endpoint answers. When `pulumi up` finishes you get the endpoints straight back as stack outputs:\n\n```bash\n<span class=\"line\"><span class=\"cl\">Outputs:\n</span></span><span class=\"line\"><span class=\"cl\"> generateEndpoint: <span class=\"s2\">\"http://<public-ip>:11434/api/generate\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> ollamaEndpoint : <span class=\"s2\">\"http://<public-ip>:11434\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> publicIp : <span class=\"s2\">\"<public-ip>\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"> tagsEndpoint : <span class=\"s2\">\"http://<public-ip>:11434/api/tags\"</span>\n</span></span>\n```\n\n## Testing the inference server\n\n`pulumi up` returns as soon as the infrastructure exists, which is before the model has finished downloading. This is exactly where Terraform reached for that `null_resource` loop, and where Pulumi hands you a URL instead. To check whether the model is ready, ask Ollama what it has loaded:\n\n```bash\n<span class=\"line\"><span class=\"cl\">curl -s <span class=\"k\">$(</span>pulumi stack output tagsEndpoint<span class=\"k\">)</span> <span class=\"p\">|</span> grep -q <span class=\"s2\">\"qwen2.5:14b\"</span> <span class=\"o\">&&</span> <span class=\"nb\">echo</span> <span class=\"s2\">\"ready\"</span> <span class=\"o\">||</span> <span class=\"nb\">echo</span> <span class=\"s2\">\"still pulling\"</span>\n</span></span>\n```\n\nOnce it reports ready, send it a prompt. This is the same request the Akamai post makes, pointed at your `generateEndpoint` output:\n\n```bash\n<span class=\"line\"><span class=\"cl\">curl -s <span class=\"k\">$(</span>pulumi stack output generateEndpoint<span class=\"k\">)</span> -d <span class=\"s1\">'{\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s1\"> \"model\": \"qwen2.5:14b\",\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s1\"> \"system\": \"Answer every question with a three-line poem.\",\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s1\"> \"prompt\": \"Why is the sky blue?\",\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s1\"> \"stream\": false\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s1\">}'</span>\n</span></span>\n```\n\nThe first call is slower, because Ollama loads the model into GPU memory before it answers. Every call after that is fast, since `OLLAMA_KEEP_ALIVE=-1` keeps it resident:\n\n```json\n<span class=\"line\"><span class=\"cl\"><span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"model\"</span><span class=\"p\">:</span> <span class=\"s2\">\"qwen2.5:14b\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"response\"</span><span class=\"p\">:</span> <span class=\"s2\">\"Sunlight scatters, short waves fly,\\nBlue light paints the open sky,\\nViolet fades as day drifts by.\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"done\"</span><span class=\"p\">:</span> <span class=\"kc\">true</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span>\n```\n\nWhen you are done, take it all down with one command, so the GPU stops billing:\n\n```bash\n<span class=\"line\"><span class=\"cl\">pulumi destroy\n</span></span>\n```\n\n## Cost\n\nA GPU instance is the whole bill, and you pay by the hour whether the model is busy or idle. The numbers below are rough on-demand rates for a single T4-class box left running 24/7; in practice you spin it up, use it, and `pulumi destroy` it, so what you actually pay tracks the hours it stays up.\n\nCloud\n\nInstance\n\nGPU\n\n~Hourly\n\n~Monthly (24/7)\n\nAWS\n\n`g4dn.xlarge`\n\n1× T4 (16 GB)\n\n~$0.53\n\n~$384\n\nAzure\n\n`Standard_NC4as_T4_v3`\n\n1× T4 (16 GB)\n\n~$0.53\n\n~$384\n\nGoogle Cloud\n\n`n1-standard-4` + 1× T4\n\n1× T4 (16 GB)\n\n~$0.54\n\n~$394\n\nThese are on-demand Linux rates in a cheap US region (`us-east-1`, `eastus`, `us-central1`) as of mid-2026, and the GPU dominates the bill. Spot or preemptible capacity cuts it sharply if your workload tolerates interruption (often 60 to 70 percent off), Google Cloud’s sustained-use discount trims an always-on instance on its own, and the 40 GB disk adds only a few dollars a month.\n\nA GPU box left running overnight is the expensive mistake here. Because the whole stack is declarative, the safe habit is cheap: `pulumi destroy` when you stop using it, `pulumi up` when you need it back. The state and config are version-controlled, so standing it up again is one command, not a rebuild.\n\n## Security considerations\n\nThe architecture in this post matches the Akamai original on purpose, which means it carries the same caveat: Ollama is exposed over plain HTTP on a port open to the entire internet. That is fine for a demo on a box you tear down the same day. It is not fine for anything that outlives the afternoon, and self-hosted AI endpoints get found fast: scanners like [Shodan](https://www.shodan.io/) enumerate a freshly exposed one within hours, and an open Ollama instance is free compute for whoever finds it first.\n\nThe credential side, though, is genuinely better than a static-token setup, and that is the part worth keeping. There is no long-lived cloud key in the program, in your shell, or in CI; every deploy gets short-lived credentials minted through `pulumi-idp/auth` and thrown away when it finishes.\n\nConcern\n\nAkamai/Terraform demo\n\nThis deployment\n\nCloud credentials\n\nStatic PAT in an env var\n\nOIDC login, short-lived, via `pulumi-idp/auth`\n\nInbound exposure\n\nPort 11434 open to `0.0.0.0/0`\n\nSame by default; one config flag scopes the CIDR\n\nTransport\n\nPlain HTTP\n\nPlain HTTP (front with TLS for real use)\n\nReadiness wait\n\n`null_resource` + `local-exec` shell loop\n\nA runtime `curl`, no resource\n\nMy recommendations for taking this past a demo:\n\n- Scope the inbound rule to your own CIDR instead of `0.0.0.0/0`; the `allowSsh` flag in the program is the pattern to copy for the Ollama port\n- Put a reverse proxy with TLS in front of Ollama, or keep the box off the public internet entirely and reach it over a private network or a [Tailscale tailnet](https://www.pulumi.com/blog/deploy-a-hermes-agent-with-pulumi/), the way I did for the Hermes agent\n- Keep credentials on OIDC through ESC; never fall back to a static key to “just get it working”\n- Treat the box as disposable, and `pulumi destroy` it when you are not using it, which shrinks both the bill and the attack window\n\n## What’s next?\n\nThe program is a foundation, and the interesting work is what you layer on once a model is a `pulumi up` away:\n\n- **Lock it down.** Scope the firewall, add TLS, or move the box behind a private network so the endpoint is not on the public internet at all.\n- **Scale the model to the GPU.** `qwen2.5:14b` on a T4 is the entry point. Bigger models want more memory, which means an A10G, an L4, or an A100, and the only change in the program is the instance type and the `model` value.\n- **Swap the serving engine.** Ollama is the simplest thing that works. For higher throughput, the same shape holds with [vLLM](https://github.com/vllm-project/vllm) in place of Ollama in the cloud-init.\n- **Generate the next one.** [Pulumi Neo](https://www.pulumi.com/product/neo/) can take a target like “a GPU inference box on Azure behind a private endpoint” and produce a first draft of the program, which you then review in a PR like any other change.\n\n## Conclusion\n\nDeclaring an AI inference server as code buys you the same thing it buys for any other infrastructure: you can reproduce it on any of the three clouds, version-control it, and tear it down with a single `pulumi destroy`. Porting the Akamai project made the contrast sharp. The credentials moved from a static token to an OIDC login that leaves nothing behind, and the readiness wait disappeared entirely once you stop treating a runtime check as a resource.\n\nThat is the line worth carrying to the next thing you deploy. Not every step in a runbook is a resource. A GPU box and a firewall rule are; waiting for a download to finish is not. Draw that line first, and the program gets shorter on its own.\n\nIf you run into issues or have questions, drop by the [Pulumi Community Slack](https://slack.pulumi.com/) or [GitHub Discussions](https://github.com/pulumi/pulumi/discussions). New to Pulumi? [Get started here](https://www.pulumi.com/docs/get-started/).","publishedAt":"2026-06-30T00:00:00.000Z","fetchedAt":"2026-06-30T13:01:14.379Z","url":"https://www.pulumi.com/blog/fully-automated-ai-inference-aws-azure-gcp-pulumi/","media":[{"type":"image","url":"https://www.pulumi.com/blog/fully-automated-ai-inference-aws-azure-gcp-pulumi/meta.png","r2Key":"releases/42e0632d3c251f96d5f83e2c51905b5b68d270a4346417479fdd4fa953ff360f.png","r2Url":"https://media.releases.sh/releases/42e0632d3c251f96d5f83e2c51905b5b68d270a4346417479fdd4fa953ff360f.png"},{"type":"image","url":"https://opengraph.githubassets.com/1/dirien/fully-automated-ai-inference-pulumi","alt":"GitHub repository: dirien/fully-automated-ai-inference-pulumi","r2Key":"releases/8adb904797283a71b969ac6ebe5f7ad12da318c08afd2612252603438b60a722.png","r2Url":"https://media.releases.sh/releases/8adb904797283a71b969ac6ebe5f7ad12da318c08afd2612252603438b60a722.png"}],"coverageCount":0},{"id":"rel_xxHQVxhu8DcXSPvEhkgHT","version":null,"type":"feature","title":"Introducing ESC Secret Rotation Webhooks","summary":"Pulumi ESC now supports webhooks that trigger when an environment’s secrets are rotated, notifying on success or failure. Configure them via the Pulumi Cloud Console, the Pulumi Service Provider, or the CLI.","titleGenerated":"Pulumi ESC introduces webhooks for secret rotation completion","titleShort":"Secret rotation webhooks notify on success or failure","breaking":"none","importance":null,"content":"![](https://www.pulumi.com/blog/introducing-esc-secret-rotation-webhooks/meta.png)\n\n[Pulumi ESC](https://www.pulumi.com/docs/esc/) centralizes your secrets and configuration, and it can [automatically rotate secrets](https://www.pulumi.com/docs/esc/concepts/rotators/) on a schedule so credentials never go stale. But a rotation is only useful if the systems that depend on it know it happened. ESC secret rotation webhooks close that gap by notifying you the moment a secret rotates.\n\n## Introducing secret rotation webhooks\n\nWith [ESC webhooks](https://www.pulumi.com/docs/esc/concepts/webhooks/), you can react to rotations automatically. When ESC rotates an environment’s secrets, a webhook can be configured to trigger on either success or failure. Use it to notify your team in Slack, refresh services that hold the old credential, or catch a failed rotation before it causes an outage.\n\n## How to configure\n\n### Pulumi Cloud Console\n\nUsing the Pulumi Cloud Console, you can now configure webhooks for “Environment rotation succeeded” and “Environment rotation failed” in your ESC Environment’s Settings page (under **Settings** -> **Notifications**).\n\n![ESC webhook trigger settings with “Environment rotation succeeded” and “Environment rotation failed” selected](./rotation-webhook-triggers.png)\n\n### Pulumi Service Provider\n\nYou can use the Pulumi Service Provider in your Pulumi program to configure webhooks. Here is an example in TypeScript:\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">environmentWebhook</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">service</span><span class=\"p\">.</span><span class=\"nx\">Webhook</span><span class=\"p\">(</span><span class=\"s2\">\"env-webhook\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">active</span>: <span class=\"kt\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">displayName</span><span class=\"o\">:</span> <span class=\"s2\">\"env-webhook\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">organizationName</span><span class=\"o\">:</span> <span class=\"s2\">\"my-org\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">projectName</span>: <span class=\"kt\">environment.project</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">environmentName</span>: <span class=\"kt\">environment.name</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">payloadUrl</span><span class=\"o\">:</span> <span class=\"s2\">\"https://example.com\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">filters</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"nx\">WebhookFilters</span><span class=\"p\">.</span><span class=\"nx\">EnvironmentRotationSucceeded</span><span class=\"p\">,</span> <span class=\"nx\">WebhookFilters</span><span class=\"p\">.</span><span class=\"nx\">EnvironmentRotationFailed</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">})</span>\n</span></span>\n```\n\n### Pulumi CLI\n\nYou can also use the Pulumi CLI to configure the webhook:\n\n```\npulumi env webhook new my-org/project/env env-webhook \\\n--url https://example.com \\\n--event environment_rotation_succeeded \\\n--event environment_rotation_failed\n```\n\n## Get started\n\nSecret rotation webhooks are available now for all Pulumi ESC environments. See the [webhooks documentation](https://www.pulumi.com/docs/esc/concepts/webhooks/) to get started, and share your feedback on our [GitHub repository](https://github.com/pulumi/esc).","publishedAt":"2026-06-26T00:00:00.000Z","fetchedAt":"2026-06-26T18:00:56.602Z","url":"https://www.pulumi.com/blog/introducing-esc-secret-rotation-webhooks/","media":[{"type":"image","url":"https://www.pulumi.com/blog/introducing-esc-secret-rotation-webhooks/meta.png","r2Key":"releases/6327df9addd20501f5fc6fc139f5210235eb8548dd94f165c09c1040fa385ec2.png","r2Url":"https://media.releases.sh/releases/6327df9addd20501f5fc6fc139f5210235eb8548dd94f165c09c1040fa385ec2.png"}],"coverageCount":0},{"id":"rel_Uh3IhexIK55Ya2RqSgzbP","version":null,"type":"feature","title":"Build a Governed Databricks Workspace with Pulumi","summary":"New guide covers provisioning a governed Databricks workspace baseline with Pulumi, including cluster policies, notebooks, secret scopes, permissions, and automated jobs.","titleGenerated":"Pulumi publishes guide to building governed Databricks workspaces","titleShort":"Governed Databricks workspace guide for platform teams","breaking":"none","importance":null,"content":"![](https://www.pulumi.com/blog/end-to-end-databricks-with-pulumi/meta.png)\n\nPlatform teams responsible for [Databricks](https://www.databricks.com/) often find themselves manually configuring clusters and notebooks for every new data science team. This manual overhead leads to inconsistent cluster policies, runaway costs from oversized instances, and security gaps in workspace operations. Without a standardized way to provision workspace-local resources, data platforms become a fragmented collection of bespoke environments that are impossible to govern at scale.\n\nAs Databricks usage grows across the enterprise, the lack of a governed workspace baseline becomes a major operational risk. Inconsistent policies lead to unpredictable billing and audit failures. Standardizing your Databricks environment with Pulumi ensures that every workspace starts with the correct cost controls, cluster policies, notebooks, and automated jobs, allowing your data teams to move faster without compromising governance.\n\n## What you’ll build\n\nIn this post, you will learn how to provision a governed Databricks workspace baseline using Pulumi. You will build:\n\n1. **Cluster policies** to enforce instance types and cost controls.\n2. **Workspace notebooks** to standardize workload locations.\n3. **Secret scopes and permissions** to make access reviewable.\n4. **Automated jobs** that run on policy-constrained compute.\n\nBy the end, you will have a reproducible workspace configuration that you can deploy to any new Databricks environment.\n\n## The Databricks management boundary\n\nWhen managing Databricks with Pulumi, understand the boundary between account-level and workspace-level resources.\n\n1. **Account-level**: Creating cloud prerequisites such as VPCs, subnets, IAM roles, and Azure resource groups uses the cloud provider for your target platform. On AWS and GCP, Databricks workspace creation and account-level identity use the Databricks provider’s account APIs, such as `databricks.MwsWorkspaces`; on Azure, the workspace itself is commonly managed with `azure-native`.\n2. **Workspace-level**: Managing clusters, jobs, notebooks, permissions, and secret scopes within a specific workspace. This example uses the `@pulumi/databricks` provider against an Azure Databricks workspace URL, and the same workspace-resource pattern applies once your provider is configured for the target workspace.\n\nFor this tutorial, we’ll assume you have an existing Databricks workspace and want to manage the resources within it.\n\n## Configuring credentials with Pulumi ESC\n\nBefore writing code, we need to configure our credentials. [Pulumi ESC](https://www.pulumi.com/docs/esc/) (Environments, Secrets, and Configuration) manages these securely.\n\nYou can create an environment that maps Databricks credentials into Pulumi configuration for the Databricks provider:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"nt\">values</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">databricks</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">host</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"https://adb-123456789.0.azuredatabricks.net\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">token</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">fn::secret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">databricks-token</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">pulumiConfig</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">databricks:host</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${databricks.host}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">databricks:token</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${databricks.token}</span><span class=\"w\">\n</span></span></span>\n```\n\nOnce configured, import this environment into your Pulumi stack. The Databricks provider reads configuration values such as [`databricks:host` and `databricks:token`](https://www.pulumi.com/registry/packages/databricks/installation-configuration/) without requiring static secrets in stack config.\n\n## Defining your data infrastructure\n\nThe following Pulumi program in TypeScript sets up separate cluster policies for interactive and job compute, a governed shared cluster, a secret scope, explicit permissions, a notebook, and a scheduled job that runs on policy-constrained compute.\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">databricks</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/databricks\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">interactivePolicy</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">databricks</span><span class=\"p\">.</span><span class=\"nx\">ClusterPolicy</span><span class=\"p\">(</span><span class=\"s2\">\"interactive-fair-use\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Interactive Fair Use Policy\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">definition</span>: <span class=\"kt\">JSON.stringify</span><span class=\"p\">({</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"dbus_per_hour\"</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"type\"</span><span class=\"o\">:</span> <span class=\"s2\">\"range\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"maxValue\"</span><span class=\"o\">:</span> <span class=\"mi\">10</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"autotermination_minutes\"</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"type\"</span><span class=\"o\">:</span> <span class=\"s2\">\"fixed\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"value\"</span><span class=\"o\">:</span> <span class=\"mi\">20</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"hidden\"</span><span class=\"o\">:</span> <span class=\"kc\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}),</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">jobPolicy</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">databricks</span><span class=\"p\">.</span><span class=\"nx\">ClusterPolicy</span><span class=\"p\">(</span><span class=\"s2\">\"job-fair-use\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Job Fair Use Policy\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">definition</span>: <span class=\"kt\">JSON.stringify</span><span class=\"p\">({</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"dbus_per_hour\"</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"type\"</span><span class=\"o\">:</span> <span class=\"s2\">\"range\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"maxValue\"</span><span class=\"o\">:</span> <span class=\"mi\">10</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}),</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">sharedCluster</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">databricks</span><span class=\"p\">.</span><span class=\"nx\">Cluster</span><span class=\"p\">(</span><span class=\"s2\">\"shared-cluster\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">clusterName</span><span class=\"o\">:</span> <span class=\"s2\">\"Shared Engineering Cluster\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">sparkVersion</span><span class=\"o\">:</span> <span class=\"s2\">\"17.3.x-scala2.13\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">nodeTypeId</span><span class=\"o\">:</span> <span class=\"s2\">\"Standard_DS3_v2\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">autoterminationMinutes</span>: <span class=\"kt\">20</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">numWorkers</span>: <span class=\"kt\">2</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">policyId</span>: <span class=\"kt\">interactivePolicy.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">clusterPermissions</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">databricks</span><span class=\"p\">.</span><span class=\"nx\">Permissions</span><span class=\"p\">(</span><span class=\"s2\">\"shared-cluster-permissions\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">clusterId</span>: <span class=\"kt\">sharedCluster.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">accessControls</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">groupName</span><span class=\"o\">:</span> <span class=\"s2\">\"data-engineers\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">permissionLevel</span><span class=\"o\">:</span> <span class=\"s2\">\"CAN_RESTART\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">groupName</span><span class=\"o\">:</span> <span class=\"s2\">\"analysts\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">permissionLevel</span><span class=\"o\">:</span> <span class=\"s2\">\"CAN_ATTACH_TO\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">secretScope</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">databricks</span><span class=\"p\">.</span><span class=\"nx\">SecretScope</span><span class=\"p\">(</span><span class=\"s2\">\"etl-secrets\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"etl-secrets\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">secretScopeAcl</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">databricks</span><span class=\"p\">.</span><span class=\"nx\">SecretAcl</span><span class=\"p\">(</span><span class=\"s2\">\"etl-secrets-readers\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">scope</span>: <span class=\"kt\">secretScope.name</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">principal</span><span class=\"o\">:</span> <span class=\"s2\">\"data-engineers\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">permission</span><span class=\"o\">:</span> <span class=\"s2\">\"READ\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">etlNotebook</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">databricks</span><span class=\"p\">.</span><span class=\"nx\">Notebook</span><span class=\"p\">(</span><span class=\"s2\">\"etl-notebook\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">path</span><span class=\"o\">:</span> <span class=\"s2\">\"/Shared/ETL/NightlyProcess\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">language</span><span class=\"o\">:</span> <span class=\"s2\">\"PYTHON\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">contentBase64</span>: <span class=\"kt\">Buffer.from</span><span class=\"p\">(</span><span class=\"s2\">\"print('Running ETL process...')\"</span><span class=\"p\">).</span><span class=\"nx\">toString</span><span class=\"p\">(</span><span class=\"s2\">\"base64\"</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">job</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">databricks</span><span class=\"p\">.</span><span class=\"nx\">Job</span><span class=\"p\">(</span><span class=\"s2\">\"nightly-etl\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Nightly ETL Job\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">tasks</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">taskKey</span><span class=\"o\">:</span> <span class=\"s2\">\"etl-task\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">newCluster</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">numWorkers</span>: <span class=\"kt\">2</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">sparkVersion</span><span class=\"o\">:</span> <span class=\"s2\">\"17.3.x-scala2.13\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">nodeTypeId</span><span class=\"o\">:</span> <span class=\"s2\">\"Standard_DS3_v2\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">policyId</span>: <span class=\"kt\">jobPolicy.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">notebookTask</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">notebookPath</span>: <span class=\"kt\">etlNotebook.path</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">schedule</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">quartzCronExpression</span><span class=\"o\">:</span> <span class=\"s2\">\"0 0 2 * * ?\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">timezoneId</span><span class=\"o\">:</span> <span class=\"s2\">\"UTC\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\nThis baseline covers the resources a platform team needs to standardize first:\n\n1. **Interactive cluster policy**: caps DBU consumption and enforces auto-termination for shared interactive compute.\n2. **Job cluster policy**: caps DBU consumption for ephemeral job compute without applying interactive auto-termination settings.\n3. **Shared cluster**: gives teams a governed interactive compute target.\n4. **Secret scope and ACL**: makes ETL secret access reviewable.\n5. **Cluster permissions**: assigns group-level access to the shared cluster.\n6. **Notebook**: stores the workload artifact in the workspace.\n7. **Job**: schedules the notebook on policy-constrained compute.\n\n## Validation\n\nAfter running `pulumi up`, you can validate your Databricks baseline:\n\n1. **Databricks UI**: Navigate to the “Compute” section and verify that the interactive and job fair-use policies exist, with auto-termination fixed only on the interactive policy.\n2. **Cluster check**: Confirm that the shared cluster references the interactive policy and uses the expected node type.\n3. **Permission check**: Confirm that cluster and secret-scope permissions match the intended Databricks groups.\n4. **Notebook check**: Confirm that `/Shared/ETL/NightlyProcess` exists in the workspace.\n5. **Job check**: Go to the “Workflows” section and confirm that the “Nightly ETL Job” is scheduled and references the notebook.\n\nManaging your Databricks workspace as code gives every team a governed, cost-aware baseline. Use Pulumi stacks to deploy the same cluster policies, permissions, secret scope, notebook, and job pattern to multiple workspaces with environment-specific configuration.\n\n```bash\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Deploy to development</span>\n</span></span><span class=\"line\"><span class=\"cl\">pulumi stack <span class=\"k\">select</span> dev\n</span></span><span class=\"line\"><span class=\"cl\">pulumi up\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Promote to production</span>\n</span></span><span class=\"line\"><span class=\"cl\">pulumi stack <span class=\"k\">select</span> prod\n</span></span><span class=\"line\"><span class=\"cl\">pulumi up\n</span></span>\n```\n\nThis keeps production ETL jobs, cluster policies, secret scopes, and permission assignments aligned with development while still letting each stack bind to its own Databricks workspace and ESC environment.\n\n## Conclusion\n\nNext, connect this workspace baseline to your CI/CD pipeline and promote it through Pulumi stacks so Databricks jobs, cluster policies, and permissions move through the same review path as application code. Start with the [Databricks provider docs](https://www.pulumi.com/registry/packages/databricks/) and [Pulumi ESC](https://www.pulumi.com/docs/esc/) for credential handling.","publishedAt":"2026-06-25T00:00:00.000Z","fetchedAt":"2026-06-25T02:01:16.960Z","url":"https://www.pulumi.com/blog/end-to-end-databricks-with-pulumi/","media":[{"type":"image","url":"https://www.pulumi.com/blog/end-to-end-databricks-with-pulumi/meta.png","r2Key":"releases/d1690fcb87c4a301c1cc7e8f741a24ca6118fd91f3df56e6abdc931e527fb95c.png","r2Url":"https://media.releases.sh/releases/d1690fcb87c4a301c1cc7e8f741a24ca6118fd91f3df56e6abdc931e527fb95c.png"}],"coverageCount":0},{"id":"rel_TQhWtRV11XSTrsdZ8yY7m","version":null,"type":"feature","title":"Deploy a Private Hermes Agent on Render Securely with Pulumi, Modal, and Tailscale","summary":"![](https://www.pulumi.com/blog/deploy-a-hermes-agent-with-pulumi/meta.png)\n\nPersonal AI agents had their breakout this year. [OpenClaw](https://www.p...","titleGenerated":null,"titleShort":null,"breaking":"unknown","importance":null,"content":"![](https://www.pulumi.com/blog/deploy-a-hermes-agent-with-pulumi/meta.png)\n\nPersonal AI agents had their breakout this year. [OpenClaw](https://www.pulumi.com/blog/deploy-openclaw-aws-hetzner/) crossed 100,000 GitHub stars within months of launching, and self-hosting your own assistant went from a hobbyist trick to something a lot of developers actually do. I wrote up how to deploy that lobster to AWS or Hetzner back when it was everywhere.\n\nThe one people are switching to now is [Hermes](https://hermes-agent.nousresearch.com/), the open-source runtime from [Nous Research](https://nousresearch.com/), and it caught on just as quickly. The reason shows up in every “I ditched OpenClaw for Hermes” thread: it actually learns, building up memory and writing its own skills as it goes instead of running off a static, human-written list.\n\nHere is the part the launch videos skip. Hermes writes and runs its own code, with no human approving the commands. A model that can write code will eventually write a bad one, and the only thing between that command and your credentials is the sandbox it runs in. That is the box you do not want on the public internet. Researchers found [175,000 exposed Ollama servers](https://thehackernews.com/2026/01/researchers-find-175000-publicly.html) sitting open in early 2026, and attackers hijack the ones they find for compute. The fix is not a better lock on the front door. It is to have no front door at all.\n\nSo this post deploys a private Hermes agent as one Pulumi program across Render, Modal, and Tailscale. The agent and its chat UI run as Render private services with no public URL, Tailscale puts the UI on your tailnet, and Modal runs the agent’s code in throwaway sandboxes. One `pulumi up` to stand it up, one `pulumi destroy` to tear it down, around $50 a month. No Makefile, no CLI escape hatches, and an honest look at the few places “nothing on the public internet” still leaks.\n\n## What is Hermes?\n\nHermes is the open-source, MIT-licensed agent runtime from [Nous Research](https://hermes-agent.nousresearch.com/hermes-agent). It runs continuously on a server rather than living in a browser tab, and you talk to it through a chat UI or a messaging platform like Telegram, Discord, Slack, or Signal. A few things make it more than a chatbot:\n\n- **It is model-agnostic.** You point it at any provider. This deployment uses [OpenRouter](https://openrouter.ai/), one key that fronts [400-plus models](https://openrouter.ai/docs/guides/overview/models) across providers, with automatic failover when one is down.\n- **It remembers across restarts.** Memory persists on disk, so it builds up context over time instead of resetting every session.\n- **It writes its own skills.** When it works through a task, it can save that approach as a reusable skill and reach for it next time.\n- **It schedules itself.** A built-in [cron](https://hermes-agent.nousresearch.com/docs/user-guide/features/cron) runs automations on a schedule, driven in plain language.\n- **It runs code in a sandbox.** When the agent needs to execute code, it hands that off to an isolated, ephemeral container instead of running it next to its own process.\n\nThe difference from a cloud-hosted assistant is the usual self-hosting trade: it runs on your infrastructure with your keys, and your conversation history and memory stay on your disk.\n\n“Hermes” here is the agent runtime from Nous Research, not the Hermes family of language models that share the name. The agent is model-agnostic, and the model it talks to is a separate choice you make through OpenRouter.\n\n## Prerequisites\n\nBefore getting started, ensure you have:\n\n- [Pulumi CLI](https://www.pulumi.com/docs/iac/download-install/) installed and configured\n- A [Pulumi Cloud account](https://app.pulumi.com/signup)\n- A [Render](https://render.com/) account, an API key, and your workspace owner ID\n- A [Modal](https://modal.com/) account and a token pair (token ID and secret)\n- An [OpenRouter](https://openrouter.ai/) API key with credit on the account\n- A container registry you can push to; this post uses GitHub Container Registry (a GHCR username and token)\n- Docker running locally (the image build rides on a local buildx engine), or a Docker-enabled CI runner\n- A [Tailscale](https://tailscale.com/) account with [HTTPS enabled](https://tailscale.com/kb/1153/enabling-https) and a reusable auth key\n- Node.js 18+ (for TypeScript) or Python 3.9+ (for Python), depending on the language you pick\n\nThis guide routes models through OpenRouter, but Hermes works with other providers too. The model is just a routing string you pass (`gpt-5.5` here), so any model OpenRouter supports works by changing that one value.\n\n## Understanding the Hermes architecture\n\nStrip away the dashboard clicks and the architecture is four pieces. One rule governs all of them: what the public internet is allowed to touch.\n\n```\nflowchart LR\nUser([Your devices]) -->|\"tailnet (WireGuard)\"| TS[\"Tailscale<br/>private mesh\"]\nTS -->|\"open-webui.<tailnet>.ts.net\"| WebUI[\"Open WebUI<br/>(Render private service,<br/>joined to your tailnet)\"]\nWebUI -->|\"private network<br/><hermes-slug>:8642\"| Hermes[\"Hermes gateway<br/>(Render private service)\"]\nHermes -->|\"Modal SDK at runtime\"| Modal[\"Modal sandbox<br/>isolated code execution\"]\nHermes -->|model calls| OR[\"OpenRouter\"]\n```\n\nComponent\n\nPort\n\nDescription\n\nHermes gateway\n\n8642\n\nHolds secrets and memory, talks to the model, creates Modal sandboxes. The most dangerous box, so it has no public URL.\n\n[Open WebUI](https://github.com/open-webui/open-webui)\n\n8080\n\nThe chat front end. Joins your tailnet and reaches Hermes over Render’s private network.\n\nModal sandbox\n\n\\-\n\nIsolated, ephemeral container for running the agent’s code.\n\nTailscale\n\n\\-\n\nA private [WireGuard mesh](https://tailscale.com/blog/how-tailscale-works) that makes the UI reachable to your devices only.\n\nThe agent that can run code and read secrets is never routable from the internet, and neither is the UI in front of it. There is no inbound ingress on any box (the outbound calls to the model, to Modal, and to the image registry still cross the internet, as they have to). Expressing “which box can reach which” in code is what makes the boundary auditable: the rule lives in the program rather than in a dashboard.\n\nThe four pieces split cleanly in two: two you declare as resources, and two you hand a credential and let run.\n\nLayer\n\nHow you provision it\n\nStatus\n\nRender services, disks, secrets\n\nofficial `render-oss` Terraform provider, bridged\n\nDeclarative, via one `pulumi package add`.\n\nContainer images\n\n`@pulumi/docker-build`\n\nBuilt and pushed during `pulumi up`. No `docker` CLI.\n\nModal sandboxes\n\nnothing\n\nNo Terraform provider exists. A runtime credential.\n\nTailnet access (Tailscale)\n\nnothing\n\nNo provider needed. A runtime credential, like Modal.\n\nTwo of these four layers are credentials, not resources. That distinction often gets missed, and it is worth getting right: a provider can only manage what it can provision, and spinning up a Modal sandbox is a runtime act, not a provisioned resource. We come back to it at each layer.\n\n## Setting up ESC for secrets management\n\nDeploying Hermes means handling a pile of credentials: a Render API key, registry credentials, the OpenRouter key, the Modal token pair, and the Tailscale auth key. You don’t want these hardcoded or scattered across environment variables. [Pulumi ESC (Environments, Secrets, and Configuration)](https://www.pulumi.com/docs/esc/) stores them securely and passes them directly to your Pulumi program.\n\nCreate a new ESC environment:\n\n```bash\n<span class=\"line\"><span class=\"cl\">pulumi env init <your-org>/hermes-secrets\n</span></span>\n```\n\nAdd your secrets to the environment:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"nt\">values</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">renderApiKey</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">fn::secret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"rnd_xxxxx\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">renderOwnerId</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"tea-xxxxx\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ghcrUser</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"your-github-username\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ghcrToken</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">fn::secret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"ghp_xxxxx\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">openrouterApiKey</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">fn::secret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"sk-or-xxxxx\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">modalTokenId</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">fn::secret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"ak-xxxxx\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">modalTokenSecret</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">fn::secret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"as-xxxxx\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">tailscaleAuthKey</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">fn::secret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"tskey-auth-xxxxx\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">tailnetDnsName</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"tailxxxxx.ts.net\"</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">pulumiConfig</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">renderApiKey</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${renderApiKey}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ownerId</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${renderOwnerId}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ghcrUser</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${ghcrUser}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ghcrToken</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${ghcrToken}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">openrouterApiKey</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${openrouterApiKey}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">modalTokenId</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${modalTokenId}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">modalTokenSecret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${modalTokenSecret}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">tailscaleAuthKey</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${tailscaleAuthKey}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">tailnetDnsName</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${tailnetDnsName}</span><span class=\"w\">\n</span></span></span>\n```\n\nTo find your tailnet DNS name, go to the [Tailscale admin console](https://login.tailscale.com/admin/dns), look under the **DNS** section, and find your tailnet name (e.g., `tailxxxxx.ts.net`). This is the domain suffix used for all machines in your Tailscale network.\n\nThen create a `Pulumi.dev.yaml` file in your project to reference the environment:\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"nt\">environment</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"l\"><your-org>/hermes-secrets</span><span class=\"w\">\n</span></span></span>\n```\n\nThis keeps your secrets out of your codebase and hands them to the program at deploy time. The Modal token pair and the Tailscale auth key never become resources; they ride straight through as encrypted env vars on the services that need them.\n\n## Generating the Render SDK\n\nRender has no first-party Pulumi provider. What it has is an official, maintained [Terraform provider](https://github.com/render-oss/terraform-provider-render), and Pulumi can turn any Terraform provider into a typed local SDK with [Any Terraform Provider](https://www.pulumi.com/docs/iac/concepts/providers/any-terraform-provider/):\n\n```bash\n<span class=\"line\"><span class=\"cl\">pulumi package add terraform-provider render-oss/render 1.8.0\n</span></span>\n```\n\nThat one command generates a `render` SDK you import like any other package, in whatever language your stack is written in. There is no published `@pulumi/render` package and no vendor to wait on.\n\nThis is also where the provider map shows its edges. The bridge wraps a Terraform provider that already exists, so it works for Render. But Modal has no Terraform provider anywhere, so `pulumi package add terraform-provider modal-labs/modal` has nothing to fetch. That is not a gap to work around. Modal is not a provisioned resource here; Hermes creates sandboxes at runtime through the Modal SDK, and Pulumi’s only job is to hold the token pair and hand it over. Tailscale lands in the same bucket: a Tailscale provider exists, but it manages your tailnet’s settings, not the act of putting a container on the network, which is a runtime job done inside the image.\n\n## Securing with Tailscale\n\nThe usual way to make a self-hosted UI reachable is to give it a public hostname and put an identity gate in front: an SSO provider, an access policy, a public DNS record, and a certificate to keep renewing. Tailscale collapses all of it into nothing you declare. There is no gate to bolt in front of a public URL, because there is no public URL. Open WebUI joins your tailnet and answers only to your own devices.\n\nThis is a stronger position than a public UI behind a login. Because there is no public URL, there is no login to harden and no certificate to renew, and Open WebUI answers only to your own devices.\n\nTailscale lives in the Open WebUI image rather than the Pulumi program. The image adds Tailscale and an entrypoint that brings up `tailscaled` in [userspace mode](https://tailscale.com/kb/1112/userspace-networking) (a container has no `/dev/net/tun`), joins your tailnet, and runs [`tailscale serve`](https://tailscale.com/kb/1312/serve) to expose Open WebUI on it. `serve` publishes over HTTPS to authenticated members of your tailnet only, where its sibling [`tailscale funnel`](https://tailscale.com/kb/1223/funnel) would put the same port on the public internet, the one thing this design avoids.\n\n```dockerfile\n<span class=\"line\"><span class=\"cl\"><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"s\">ghcr.io/open-webui/open-webui:main-slim</span><span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">RUN</span> apt-get update <span class=\"o\">&&</span> apt-get install -y --no-install-recommends curl ca-certificates <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"o\">&&</span> curl -fsSL https://tailscale.com/install.sh <span class=\"p\">|</span> sh<span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">COPY</span> entrypoint.sh /usr/local/bin/ts-entrypoint<span class=\"err\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">ENTRYPOINT</span> <span class=\"p\">[</span><span class=\"s2\">\"/usr/local/bin/ts-entrypoint\"</span><span class=\"p\">]</span><span class=\"err\">\n</span></span></span>\n```\n\n```bash\n<span class=\"line\"><span class=\"cl\"><span class=\"cp\">#!/usr/bin/env bash\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nb\">set</span> -e\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"nv\">PORT</span><span class=\"o\">=</span><span class=\"s2\">\"</span><span class=\"si\">${</span><span class=\"nv\">PORT</span><span class=\"k\">:-</span><span class=\"nv\">8080</span><span class=\"si\">}</span><span class=\"s2\">\"</span>\n</span></span><span class=\"line\"><span class=\"cl\">mkdir -p /app/backend/data/tailscale\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Userspace networking: a container has no /dev/net/tun. Keep node state on the disk</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># so it survives restarts, and accept-dns=false so Render's DNS still resolves the</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># private Hermes hostname.</span>\n</span></span><span class=\"line\"><span class=\"cl\">tailscaled --tun<span class=\"o\">=</span>userspace-networking --statedir<span class=\"o\">=</span>/app/backend/data/tailscale <span class=\"p\">&</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"nv\">n</span><span class=\"o\">=</span><span class=\"m\">0</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">until</span> tailscale up --authkey<span class=\"o\">=</span><span class=\"s2\">\"</span><span class=\"si\">${</span><span class=\"nv\">TS_AUTHKEY</span><span class=\"si\">}</span><span class=\"s2\">\"</span> --hostname<span class=\"o\">=</span><span class=\"s2\">\"</span><span class=\"si\">${</span><span class=\"nv\">TS_HOSTNAME</span><span class=\"si\">}</span><span class=\"s2\">\"</span> --accept-dns<span class=\"o\">=</span>false<span class=\"p\">;</span> <span class=\"k\">do</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nv\">n</span><span class=\"o\">=</span><span class=\"k\">$((</span>n <span class=\"o\">+</span> <span class=\"m\">1</span><span class=\"k\">))</span><span class=\"p\">;</span> <span class=\"o\">[</span> <span class=\"s2\">\"</span><span class=\"si\">${</span><span class=\"nv\">n</span><span class=\"si\">}</span><span class=\"s2\">\"</span> -ge <span class=\"m\">30</span> <span class=\"o\">]</span> <span class=\"o\">&&</span> <span class=\"o\">{</span> <span class=\"nb\">echo</span> <span class=\"s2\">\"tailnet join failed; check TS_AUTHKEY\"</span><span class=\"p\">;</span> <span class=\"nb\">exit</span> 1<span class=\"p\">;</span> <span class=\"o\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"> sleep <span class=\"m\">2</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">done</span>\n</span></span><span class=\"line\"><span class=\"cl\">tailscale serve --bg <span class=\"s2\">\"</span><span class=\"si\">${</span><span class=\"nv\">PORT</span><span class=\"si\">}</span><span class=\"s2\">\"</span> <span class=\"c1\"># serve Open WebUI on the tailnet over HTTPS</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"nb\">cd</span> /app/backend <span class=\"o\">&&</span> <span class=\"nb\">exec</span> bash start.sh\n</span></span>\n```\n\nThe `TS_AUTHKEY` env var on the service, pulled from ESC, is the only thing Pulumi contributes; everything else is the image doing its own networking.\n\nThere is one prerequisite, the same one any Tailscale node needs for `serve`: enable HTTPS for your tailnet once in the admin console so it can get a certificate. Use a **reusable** auth key, since the service re-authenticates on every redeploy, and the join loop has a ceiling so a dead key fails the deploy in a minute instead of hanging it.\n\nTo generate the reusable auth key:\n\n1. Go to the [Tailscale admin console](https://login.tailscale.com/admin/settings/keys).\n2. Click **Generate auth key**.\n3. Enable **Reusable**, since the service re-authenticates on every redeploy.\n4. Copy the key into your ESC environment as `tailscaleAuthKey`.\n\n## Deploying the agent\n\nThis is the part a Makefile usually handles. Two private services, two disks, every secret an encrypted env var, and both images built and pushed during the same `pulumi up` that creates the services. No CLI runs anywhere, though the build still rides on a local Docker engine through [buildx](https://docs.docker.com/build/concepts/overview/): the `docker` call is gone, the daemon it talks to is not, so `pulumi up` needs Docker running.\n\nCreate a new Pulumi project (pick the language you prefer; the companion repo has all three):\n\n```bash\n<span class=\"line\"><span class=\"cl\">mkdir hermes-agent-pulumi <span class=\"o\">&&</span> <span class=\"nb\">cd</span> hermes-agent-pulumi\n</span></span><span class=\"line\"><span class=\"cl\">pulumi new typescript\n</span></span>\n```\n\nInstall the dependencies and generate the Render SDK:\n\n```bash\n<span class=\"line\"><span class=\"cl\">npm install @pulumi/docker-build @pulumi/random\n</span></span><span class=\"line\"><span class=\"cl\">pulumi package add terraform-provider render-oss/render 1.8.0\n</span></span>\n```\n\nBoth services need the 2 GB Standard plan. The gateway and Open WebUI each exhaust memory on the 512 MB Starter and OOM on boot.\n\nTwo facts make this clean. Render env var values are encrypted at rest, so a secret is only an env var, with no separate resource. And [`@pulumi/docker-build`](https://www.pulumi.com/registry/packages/docker-build/) builds and pushes an image through an embedded buildx during `pulumi up`, so the image pipeline is declarative too. Render then pulls the pushed image by digest.\n\n### The complete program\n\nHermes comes first: a private Render service with no public URL, a disk for its memory, and every secret as an encrypted env var. The shared bearer key is generated in state with [`random.RandomBytes`](https://www.pulumi.com/registry/packages/random/api-docs/randombytes/), the clean replacement for `openssl rand -base64 32`; the Modal token pair and the OpenRouter key come from [config](https://www.pulumi.com/docs/iac/concepts/config/) or [Pulumi ESC](https://www.pulumi.com/docs/esc/). Render has no port field, so Hermes binds `0.0.0.0:8642` and Open WebUI reaches it at the service’s read-only `slug`. Open WebUI is almost the mirror image, private too, except its image joins your tailnet (the `tailscaled` entrypoint from the previous section) and it carries the operational env vars that keep it lean: no local embedding models, and `ENABLE_PERSISTENT_CONFIG=false` so your config always wins over a stale copy in its database. One [`render.Provider`](https://www.pulumi.com/docs/iac/concepts/resources/providers/) carries your `renderApiKey` and `ownerId` into every Render resource.\n\nHere is the whole program, copy-paste ready as your `index.ts` (or `__main__.py`, or `Pulumi.yaml`):\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">pulumi</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/pulumi\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">dockerbuild</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/docker-build\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">random</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/random\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Local SDK generated by: pulumi package add terraform-provider render-oss/render 1.8.0\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// (no @pulumi/render is published; the SDK is bridged from the official Render provider).\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">render</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/render\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">cfg</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">Config</span><span class=\"p\">();</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// ---------------------------------------------------------------------------\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Configuration. Non-secret values via `pulumi config set`, secrets via\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// `pulumi config set --secret` or a Pulumi ESC environment (see README).\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// The Render provider reads renderApiKey/renderOwnerId (or RENDER_API_KEY/RENDER_OWNER_ID).\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// ---------------------------------------------------------------------------\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">region</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"kr\">get</span><span class=\"p\">(</span><span class=\"s2\">\"renderRegion\"</span><span class=\"p\">)</span> <span class=\"o\">??</span> <span class=\"s2\">\"oregon\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">modelDefault</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"kr\">get</span><span class=\"p\">(</span><span class=\"s2\">\"modelDefault\"</span><span class=\"p\">)</span> <span class=\"o\">??</span> <span class=\"s2\">\"gpt-5.5\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Container registry the images are pushed to and Render pulls from (GHCR here).\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">ghcrUser</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"kr\">require</span><span class=\"p\">(</span><span class=\"s2\">\"ghcrUser\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">ghcrToken</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"nx\">requireSecret</span><span class=\"p\">(</span><span class=\"s2\">\"ghcrToken\"</span><span class=\"p\">);</span> <span class=\"c1\">// GHCR PAT with write:packages\n</span></span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Long-lived secrets supplied by you (dashboard-minted). Hold in config or ESC.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">modalTokenId</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"nx\">requireSecret</span><span class=\"p\">(</span><span class=\"s2\">\"modalTokenId\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">modalTokenSecret</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"nx\">requireSecret</span><span class=\"p\">(</span><span class=\"s2\">\"modalTokenSecret\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">openrouterApiKey</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"nx\">requireSecret</span><span class=\"p\">(</span><span class=\"s2\">\"openrouterApiKey\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Tailscale: the only \"gateway\". Open WebUI joins your tailnet and is reachable\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// at https://<hostname>.<tailnet>.ts.net by your devices only — never public.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Both values are already in your ESC. Enable HTTPS once in the Tailscale admin console.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">tailscaleAuthKey</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"nx\">requireSecret</span><span class=\"p\">(</span><span class=\"s2\">\"tailscaleAuthKey\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">tailnetDnsName</span> <span class=\"o\">=</span> <span class=\"nx\">cfg</span><span class=\"p\">.</span><span class=\"kr\">require</span><span class=\"p\">(</span><span class=\"s2\">\"tailnetDnsName\"</span><span class=\"p\">);</span> <span class=\"c1\">// e.g. tailc6fb4e.ts.net\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">webuiHostname</span> <span class=\"o\">=</span> <span class=\"s2\">\"open-webui\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">webuiTailnetUrl</span> <span class=\"o\">=</span> <span class=\"sb\">`https://</span><span class=\"si\">${</span><span class=\"nx\">webuiHostname</span><span class=\"si\">}</span><span class=\"sb\">.</span><span class=\"si\">${</span><span class=\"nx\">tailnetDnsName</span><span class=\"si\">}</span><span class=\"sb\">`</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Render provider auth (from ESC: pulumiConfig.renderApiKey / pulumiConfig.renderOwnerId).\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">renderProvider</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">render</span><span class=\"p\">.</span><span class=\"nx\">Provider</span><span class=\"p\">(</span><span class=\"s2\">\"render\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">apiKey</span>: <span class=\"kt\">cfg.requireSecret</span><span class=\"p\">(</span><span class=\"s2\">\"renderApiKey\"</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ownerId</span>: <span class=\"kt\">cfg.require</span><span class=\"p\">(</span><span class=\"s2\">\"renderOwnerId\"</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// ---------------------------------------------------------------------------\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Secrets generated in state (the in-IaC replacement for `openssl rand -base64 32`).\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// The shared bearer authenticates Open WebUI -> Hermes; it lands on both services.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// ---------------------------------------------------------------------------\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">sharedKey</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">random</span><span class=\"p\">.</span><span class=\"nx\">RandomBytes</span><span class=\"p\">(</span><span class=\"s2\">\"hermes-shared-key\"</span><span class=\"p\">,</span> <span class=\"p\">{</span> <span class=\"nx\">length</span>: <span class=\"kt\">32</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">webuiSecretKey</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">random</span><span class=\"p\">.</span><span class=\"nx\">RandomBytes</span><span class=\"p\">(</span><span class=\"s2\">\"webui-secret-key\"</span><span class=\"p\">,</span> <span class=\"p\">{</span> <span class=\"nx\">length</span>: <span class=\"kt\">32</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// ---------------------------------------------------------------------------\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Build and push both images during `pulumi up` (no docker CLI; the provider\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// embeds buildx). Render then pulls them by their digest-pinned ref.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// ---------------------------------------------------------------------------\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">registryAuth</span> <span class=\"o\">=</span> <span class=\"p\">[{</span> <span class=\"nx\">address</span><span class=\"o\">:</span> <span class=\"s2\">\"ghcr.io\"</span><span class=\"p\">,</span> <span class=\"nx\">username</span>: <span class=\"kt\">ghcrUser</span><span class=\"p\">,</span> <span class=\"nx\">password</span>: <span class=\"kt\">ghcrToken</span> <span class=\"p\">}];</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">hermesImage</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">dockerbuild</span><span class=\"p\">.</span><span class=\"nx\">Image</span><span class=\"p\">(</span><span class=\"s2\">\"hermes-image\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">context</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">location</span><span class=\"o\">:</span> <span class=\"s2\">\"./hermes\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">dockerfile</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">location</span><span class=\"o\">:</span> <span class=\"s2\">\"./hermes/Dockerfile\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">platforms</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"linux/amd64\"</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">tags</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">interpolate</span><span class=\"sb\">`ghcr.io/</span><span class=\"si\">${</span><span class=\"nx\">ghcrUser</span><span class=\"si\">}</span><span class=\"sb\">/hermes-agent:latest`</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">push</span>: <span class=\"kt\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">registries</span>: <span class=\"kt\">registryAuth</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">webuiImage</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">dockerbuild</span><span class=\"p\">.</span><span class=\"nx\">Image</span><span class=\"p\">(</span><span class=\"s2\">\"webui-image\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">context</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">location</span><span class=\"o\">:</span> <span class=\"s2\">\"./openwebui\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">dockerfile</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">location</span><span class=\"o\">:</span> <span class=\"s2\">\"./openwebui/Dockerfile\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">platforms</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"linux/amd64\"</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">tags</span><span class=\"o\">:</span> <span class=\"p\">[</span><span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">interpolate</span><span class=\"sb\">`ghcr.io/</span><span class=\"si\">${</span><span class=\"nx\">ghcrUser</span><span class=\"si\">}</span><span class=\"sb\">/open-webui:latest`</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">push</span>: <span class=\"kt\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">registries</span>: <span class=\"kt\">registryAuth</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Credential Render uses to pull the private GHCR images.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">ghcrCredential</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">render</span><span class=\"p\">.</span><span class=\"nx\">RegistryCredential</span><span class=\"p\">(</span><span class=\"s2\">\"ghcr\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"ghcr\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">registry</span><span class=\"o\">:</span> <span class=\"s2\">\"GITHUB\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">username</span>: <span class=\"kt\">ghcrUser</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">authToken</span>: <span class=\"kt\">ghcrToken</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">},</span> <span class=\"p\">{</span> <span class=\"nx\">provider</span>: <span class=\"kt\">renderProvider</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// ---------------------------------------------------------------------------\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Hermes: a PRIVATE Render service. No public URL; reachable only over Render's\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// private network. A disk holds its memory and workspace. Env values are\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// encrypted at rest, which is how secrets ride on Render (no separate flag).\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// ---------------------------------------------------------------------------\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">hermes</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">render</span><span class=\"p\">.</span><span class=\"nx\">PrivateService</span><span class=\"p\">(</span><span class=\"s2\">\"hermes-agent\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"hermes-agent\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">plan</span><span class=\"o\">:</span> <span class=\"s2\">\"standard\"</span><span class=\"p\">,</span> <span class=\"c1\">// 2 GB; the gateway OOMs on starter's 512 MB\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">region</span>: <span class=\"kt\">region</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">runtimeSource</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Render wants the bare repo in imageUrl; the tag/digest go in their own field.\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">image</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">imageUrl</span>: <span class=\"kt\">pulumi.interpolate</span><span class=\"sb\">`ghcr.io/</span><span class=\"si\">${</span><span class=\"nx\">ghcrUser</span><span class=\"si\">}</span><span class=\"sb\">/hermes-agent`</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">digest</span>: <span class=\"kt\">hermesImage.digest</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">registryCredentialId</span>: <span class=\"kt\">ghcrCredential.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">disk</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"hermes-data\"</span><span class=\"p\">,</span> <span class=\"nx\">mountPath</span><span class=\"o\">:</span> <span class=\"s2\">\"/opt/data\"</span><span class=\"p\">,</span> <span class=\"nx\">sizeGb</span>: <span class=\"kt\">5</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">envVars</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">PORT</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"8642\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">API_SERVER_ENABLED</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"true\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">API_SERVER_HOST</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"0.0.0.0\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">API_SERVER_PORT</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"8642\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">API_SERVER_MODEL_NAME</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span>: <span class=\"kt\">modelDefault</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Auto-approve the agent's shell/terminal hooks (no TTY in a container),\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// so it can run code in the Modal sandbox without an interactive prompt.\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">HERMES_ACCEPT_HOOKS</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"1\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Secrets (encrypted at rest on Render):\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">API_SERVER_KEY</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span>: <span class=\"kt\">sharedKey.base64</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">OPENROUTER_API_KEY</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span>: <span class=\"kt\">openrouterApiKey</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Modal is a runtime credential, not a provisioned resource: Hermes creates\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// sandboxes at runtime with this token pair. No Modal provider, no Modal CLI.\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">MODAL_TOKEN_ID</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span>: <span class=\"kt\">modalTokenId</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">MODAL_TOKEN_SECRET</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span>: <span class=\"kt\">modalTokenSecret</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">},</span> <span class=\"p\">{</span> <span class=\"nx\">provider</span>: <span class=\"kt\">renderProvider</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Open WebUI reaches Hermes at its Render-assigned internal hostname (the read-only\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// slug), not the bare service name. Same region + same workspace share the network.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">hermesApiBaseUrl</span> <span class=\"o\">=</span> <span class=\"nx\">pulumi</span><span class=\"p\">.</span><span class=\"nx\">interpolate</span><span class=\"sb\">`http://</span><span class=\"si\">${</span><span class=\"nx\">hermes</span><span class=\"p\">.</span><span class=\"nx\">slug</span><span class=\"si\">}</span><span class=\"sb\">:8642/v1`</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// ---------------------------------------------------------------------------\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Open WebUI: also a PRIVATE Render service, but the container joins your tailnet\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// (tailscaled in userspace + `tailscale serve`), so you reach it at\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// https://open-webui.<tailnet>.ts.net from your devices. Never public; the gate\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// is tailnet membership. It still reaches Hermes over Render's private network.\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// ---------------------------------------------------------------------------\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">webui</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">render</span><span class=\"p\">.</span><span class=\"nx\">PrivateService</span><span class=\"p\">(</span><span class=\"s2\">\"open-webui\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"open-webui\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">plan</span><span class=\"o\">:</span> <span class=\"s2\">\"standard\"</span><span class=\"p\">,</span> <span class=\"c1\">// 2 GB; Open WebUI OOMs on starter's 512 MB\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">region</span>: <span class=\"kt\">region</span><span class=\"p\">,</span> <span class=\"c1\">// must match Hermes for private networking\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">runtimeSource</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">image</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">imageUrl</span>: <span class=\"kt\">pulumi.interpolate</span><span class=\"sb\">`ghcr.io/</span><span class=\"si\">${</span><span class=\"nx\">ghcrUser</span><span class=\"si\">}</span><span class=\"sb\">/open-webui`</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">digest</span>: <span class=\"kt\">webuiImage.digest</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">registryCredentialId</span>: <span class=\"kt\">ghcrCredential.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">disk</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"openwebui-data\"</span><span class=\"p\">,</span> <span class=\"nx\">mountPath</span><span class=\"o\">:</span> <span class=\"s2\">\"/app/backend/data\"</span><span class=\"p\">,</span> <span class=\"nx\">sizeGb</span>: <span class=\"kt\">5</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">envVars</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">PORT</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"8080\"</span> <span class=\"p\">},</span> <span class=\"c1\">// tailscale serve points at this; Open WebUI binds it\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">OPENAI_API_BASE_URL</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span>: <span class=\"kt\">hermesApiBaseUrl</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ENABLE_OPENAI_API</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"true\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ENABLE_OLLAMA_API</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"false\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">DEFAULT_MODELS</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span>: <span class=\"kt\">modelDefault</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">WEBUI_AUTH</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"true\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">WEBUI_URL</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span>: <span class=\"kt\">webuiTailnetUrl</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Reached only over the tailnet, so the first sign-up is the admin.\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ENABLE_LOGIN_FORM</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"true\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ENABLE_SIGNUP</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"true\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">DEFAULT_USER_ROLE</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"admin\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Always apply env config; otherwise Open WebUI persists the first run's config\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// to its DB and ignores later changes.\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ENABLE_PERSISTENT_CONFIG</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"false\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Don't load local embedding/RAG models (a big memory saver; the agent is the brain).\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">RAG_EMBEDDING_ENGINE</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"openai\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">BYPASS_EMBEDDING_AND_RETRIEVAL</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"true\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">OFFLINE_MODE</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"true\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">HF_HUB_OFFLINE</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span><span class=\"o\">:</span> <span class=\"s2\">\"1\"</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Secrets (encrypted at rest):\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">OPENAI_API_KEY</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span>: <span class=\"kt\">sharedKey.base64</span> <span class=\"p\">},</span> <span class=\"c1\">// shared bearer to call Hermes\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">WEBUI_SECRET_KEY</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span>: <span class=\"kt\">webuiSecretKey.base64</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\">// Tailscale joins this container to your tailnet and serves Open WebUI on it.\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">TS_AUTHKEY</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span>: <span class=\"kt\">tailscaleAuthKey</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">TS_HOSTNAME</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span>: <span class=\"kt\">webuiHostname</span> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">},</span> <span class=\"p\">{</span> <span class=\"nx\">dependsOn</span>: <span class=\"kt\">hermes</span><span class=\"p\">,</span> <span class=\"nx\">provider</span>: <span class=\"kt\">renderProvider</span> <span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">hermesInternalUrl</span> <span class=\"o\">=</span> <span class=\"nx\">hermesApiBaseUrl</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">// Open this from any device on your tailnet:\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">export</span> <span class=\"kr\">const</span> <span class=\"nx\">openWebUiTailnetUrl</span> <span class=\"o\">=</span> <span class=\"nx\">webuiTailnetUrl</span><span class=\"p\">;</span>\n</span></span>\n```\n\n```python\n<span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">pulumi</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">pulumi_docker_build</span> <span class=\"k\">as</span> <span class=\"nn\">docker_build</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">pulumi_random</span> <span class=\"k\">as</span> <span class=\"nn\">random</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Local SDK generated by: pulumi package add terraform-provider render-oss/render 1.8.0</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># (no pulumi_render is published; the SDK is bridged from the official Render provider).</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kn\">import</span> <span class=\"nn\">pulumi_render</span> <span class=\"k\">as</span> <span class=\"nn\">render</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">cfg</span> <span class=\"o\">=</span> <span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">Config</span><span class=\"p\">()</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Configuration. Non-secret values via `pulumi config set`, secrets via</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># `pulumi config set --secret` or a Pulumi ESC environment (see README).</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># The Render provider reads renderApiKey/renderOwnerId (or RENDER_API_KEY/RENDER_OWNER_ID).</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">region</span> <span class=\"o\">=</span> <span class=\"n\">cfg</span><span class=\"o\">.</span><span class=\"n\">get</span><span class=\"p\">(</span><span class=\"s2\">\"renderRegion\"</span><span class=\"p\">)</span> <span class=\"ow\">or</span> <span class=\"s2\">\"oregon\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">model_default</span> <span class=\"o\">=</span> <span class=\"n\">cfg</span><span class=\"o\">.</span><span class=\"n\">get</span><span class=\"p\">(</span><span class=\"s2\">\"modelDefault\"</span><span class=\"p\">)</span> <span class=\"ow\">or</span> <span class=\"s2\">\"gpt-5.5\"</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Container registry the images are pushed to and Render pulls from (GHCR here).</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">ghcr_user</span> <span class=\"o\">=</span> <span class=\"n\">cfg</span><span class=\"o\">.</span><span class=\"n\">require</span><span class=\"p\">(</span><span class=\"s2\">\"ghcrUser\"</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">ghcr_token</span> <span class=\"o\">=</span> <span class=\"n\">cfg</span><span class=\"o\">.</span><span class=\"n\">require_secret</span><span class=\"p\">(</span><span class=\"s2\">\"ghcrToken\"</span><span class=\"p\">)</span> <span class=\"c1\"># GHCR PAT with write:packages</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Long-lived secrets supplied by you (dashboard-minted). Hold in config or ESC.</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">modal_token_id</span> <span class=\"o\">=</span> <span class=\"n\">cfg</span><span class=\"o\">.</span><span class=\"n\">require_secret</span><span class=\"p\">(</span><span class=\"s2\">\"modalTokenId\"</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">modal_token_secret</span> <span class=\"o\">=</span> <span class=\"n\">cfg</span><span class=\"o\">.</span><span class=\"n\">require_secret</span><span class=\"p\">(</span><span class=\"s2\">\"modalTokenSecret\"</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">openrouter_api_key</span> <span class=\"o\">=</span> <span class=\"n\">cfg</span><span class=\"o\">.</span><span class=\"n\">require_secret</span><span class=\"p\">(</span><span class=\"s2\">\"openrouterApiKey\"</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Tailscale: the only \"gateway\". Open WebUI joins your tailnet and is reachable</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># at https://<hostname>.<tailnet>.ts.net by your devices only -- never public.</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Enable HTTPS once in the Tailscale admin console so `tailscale serve` can get a cert.</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">tailscale_auth_key</span> <span class=\"o\">=</span> <span class=\"n\">cfg</span><span class=\"o\">.</span><span class=\"n\">require_secret</span><span class=\"p\">(</span><span class=\"s2\">\"tailscaleAuthKey\"</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">tailnet_dns_name</span> <span class=\"o\">=</span> <span class=\"n\">cfg</span><span class=\"o\">.</span><span class=\"n\">require</span><span class=\"p\">(</span><span class=\"s2\">\"tailnetDnsName\"</span><span class=\"p\">)</span> <span class=\"c1\"># e.g. tailc6fb4e.ts.net</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">webui_hostname</span> <span class=\"o\">=</span> <span class=\"s2\">\"open-webui\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">webui_tailnet_url</span> <span class=\"o\">=</span> <span class=\"sa\">f</span><span class=\"s2\">\"https://</span><span class=\"si\">{</span><span class=\"n\">webui_hostname</span><span class=\"si\">}</span><span class=\"s2\">.</span><span class=\"si\">{</span><span class=\"n\">tailnet_dns_name</span><span class=\"si\">}</span><span class=\"s2\">\"</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Render provider auth (from ESC: pulumiConfig.renderApiKey / pulumiConfig.renderOwnerId).</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">render_provider</span> <span class=\"o\">=</span> <span class=\"n\">render</span><span class=\"o\">.</span><span class=\"n\">Provider</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"render\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">api_key</span><span class=\"o\">=</span><span class=\"n\">cfg</span><span class=\"o\">.</span><span class=\"n\">require_secret</span><span class=\"p\">(</span><span class=\"s2\">\"renderApiKey\"</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">owner_id</span><span class=\"o\">=</span><span class=\"n\">cfg</span><span class=\"o\">.</span><span class=\"n\">require</span><span class=\"p\">(</span><span class=\"s2\">\"renderOwnerId\"</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Secrets generated in state (the in-IaC replacement for `openssl rand -base64 32`).</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># The shared bearer authenticates Open WebUI -> Hermes; it lands on both services.</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">shared_key</span> <span class=\"o\">=</span> <span class=\"n\">random</span><span class=\"o\">.</span><span class=\"n\">RandomBytes</span><span class=\"p\">(</span><span class=\"s2\">\"hermes-shared-key\"</span><span class=\"p\">,</span> <span class=\"n\">length</span><span class=\"o\">=</span><span class=\"mi\">32</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">webui_secret_key</span> <span class=\"o\">=</span> <span class=\"n\">random</span><span class=\"o\">.</span><span class=\"n\">RandomBytes</span><span class=\"p\">(</span><span class=\"s2\">\"webui-secret-key\"</span><span class=\"p\">,</span> <span class=\"n\">length</span><span class=\"o\">=</span><span class=\"mi\">32</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Build and push both images during `pulumi up` (no docker CLI; the provider</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># embeds buildx). Render then pulls them by their digest-pinned ref.</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">registry_auth</span> <span class=\"o\">=</span> <span class=\"p\">[{</span><span class=\"s2\">\"address\"</span><span class=\"p\">:</span> <span class=\"s2\">\"ghcr.io\"</span><span class=\"p\">,</span> <span class=\"s2\">\"username\"</span><span class=\"p\">:</span> <span class=\"n\">ghcr_user</span><span class=\"p\">,</span> <span class=\"s2\">\"password\"</span><span class=\"p\">:</span> <span class=\"n\">ghcr_token</span><span class=\"p\">}]</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">hermes_image</span> <span class=\"o\">=</span> <span class=\"n\">docker_build</span><span class=\"o\">.</span><span class=\"n\">Image</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"hermes-image\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">context</span><span class=\"o\">=</span><span class=\"p\">{</span><span class=\"s2\">\"location\"</span><span class=\"p\">:</span> <span class=\"s2\">\"./hermes\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">dockerfile</span><span class=\"o\">=</span><span class=\"p\">{</span><span class=\"s2\">\"location\"</span><span class=\"p\">:</span> <span class=\"s2\">\"./hermes/Dockerfile\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">platforms</span><span class=\"o\">=</span><span class=\"p\">[</span><span class=\"s2\">\"linux/amd64\"</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">tags</span><span class=\"o\">=</span><span class=\"p\">[</span><span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">Output</span><span class=\"o\">.</span><span class=\"n\">concat</span><span class=\"p\">(</span><span class=\"s2\">\"ghcr.io/\"</span><span class=\"p\">,</span> <span class=\"n\">ghcr_user</span><span class=\"p\">,</span> <span class=\"s2\">\"/hermes-agent:latest\"</span><span class=\"p\">)],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">push</span><span class=\"o\">=</span><span class=\"kc\">True</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">registries</span><span class=\"o\">=</span><span class=\"n\">registry_auth</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">webui_image</span> <span class=\"o\">=</span> <span class=\"n\">docker_build</span><span class=\"o\">.</span><span class=\"n\">Image</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"webui-image\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">context</span><span class=\"o\">=</span><span class=\"p\">{</span><span class=\"s2\">\"location\"</span><span class=\"p\">:</span> <span class=\"s2\">\"./openwebui\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">dockerfile</span><span class=\"o\">=</span><span class=\"p\">{</span><span class=\"s2\">\"location\"</span><span class=\"p\">:</span> <span class=\"s2\">\"./openwebui/Dockerfile\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">platforms</span><span class=\"o\">=</span><span class=\"p\">[</span><span class=\"s2\">\"linux/amd64\"</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">tags</span><span class=\"o\">=</span><span class=\"p\">[</span><span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">Output</span><span class=\"o\">.</span><span class=\"n\">concat</span><span class=\"p\">(</span><span class=\"s2\">\"ghcr.io/\"</span><span class=\"p\">,</span> <span class=\"n\">ghcr_user</span><span class=\"p\">,</span> <span class=\"s2\">\"/open-webui:latest\"</span><span class=\"p\">)],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">push</span><span class=\"o\">=</span><span class=\"kc\">True</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">registries</span><span class=\"o\">=</span><span class=\"n\">registry_auth</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Credential Render uses to pull the private GHCR images.</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">ghcr_credential</span> <span class=\"o\">=</span> <span class=\"n\">render</span><span class=\"o\">.</span><span class=\"n\">RegistryCredential</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"ghcr\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">name</span><span class=\"o\">=</span><span class=\"s2\">\"ghcr\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">registry</span><span class=\"o\">=</span><span class=\"s2\">\"GITHUB\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">username</span><span class=\"o\">=</span><span class=\"n\">ghcr_user</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">auth_token</span><span class=\"o\">=</span><span class=\"n\">ghcr_token</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">opts</span><span class=\"o\">=</span><span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">ResourceOptions</span><span class=\"p\">(</span><span class=\"n\">provider</span><span class=\"o\">=</span><span class=\"n\">render_provider</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Hermes: a PRIVATE Render service. No public URL; reachable only over Render's</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># private network. A disk holds its memory and workspace. Env values are</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># encrypted at rest, which is how secrets ride on Render (no separate flag).</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">hermes</span> <span class=\"o\">=</span> <span class=\"n\">render</span><span class=\"o\">.</span><span class=\"n\">PrivateService</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"hermes-agent\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">name</span><span class=\"o\">=</span><span class=\"s2\">\"hermes-agent\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">plan</span><span class=\"o\">=</span><span class=\"s2\">\"standard\"</span><span class=\"p\">,</span> <span class=\"c1\"># 2 GB; the gateway OOMs on starter's 512 MB</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">region</span><span class=\"o\">=</span><span class=\"n\">region</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">runtime_source</span><span class=\"o\">=</span><span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"image\"</span><span class=\"p\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"image_url\"</span><span class=\"p\">:</span> <span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">Output</span><span class=\"o\">.</span><span class=\"n\">concat</span><span class=\"p\">(</span><span class=\"s2\">\"ghcr.io/\"</span><span class=\"p\">,</span> <span class=\"n\">ghcr_user</span><span class=\"p\">,</span> <span class=\"s2\">\"/hermes-agent\"</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"digest\"</span><span class=\"p\">:</span> <span class=\"n\">hermes_image</span><span class=\"o\">.</span><span class=\"n\">digest</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"registry_credential_id\"</span><span class=\"p\">:</span> <span class=\"n\">ghcr_credential</span><span class=\"o\">.</span><span class=\"n\">id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">disk</span><span class=\"o\">=</span><span class=\"p\">{</span><span class=\"s2\">\"name\"</span><span class=\"p\">:</span> <span class=\"s2\">\"hermes-data\"</span><span class=\"p\">,</span> <span class=\"s2\">\"mount_path\"</span><span class=\"p\">:</span> <span class=\"s2\">\"/opt/data\"</span><span class=\"p\">,</span> <span class=\"s2\">\"size_gb\"</span><span class=\"p\">:</span> <span class=\"mi\">5</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">env_vars</span><span class=\"o\">=</span><span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"PORT\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"8642\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"API_SERVER_ENABLED\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"true\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"API_SERVER_HOST\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"0.0.0.0\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"API_SERVER_PORT\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"8642\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"API_SERVER_MODEL_NAME\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"n\">model_default</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\"># Auto-approve the agent's shell/terminal hooks (no TTY in a container).</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"HERMES_ACCEPT_HOOKS\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"1\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\"># Secrets (encrypted at rest on Render):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"API_SERVER_KEY\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"n\">shared_key</span><span class=\"o\">.</span><span class=\"n\">base64</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"OPENROUTER_API_KEY\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"n\">openrouter_api_key</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\"># Modal is a runtime credential: Hermes creates sandboxes at runtime with this pair.</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"MODAL_TOKEN_ID\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"n\">modal_token_id</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"MODAL_TOKEN_SECRET\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"n\">modal_token_secret</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">opts</span><span class=\"o\">=</span><span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">ResourceOptions</span><span class=\"p\">(</span><span class=\"n\">provider</span><span class=\"o\">=</span><span class=\"n\">render_provider</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Open WebUI reaches Hermes at its Render-assigned internal hostname (the read-only slug).</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">hermes_api_base_url</span> <span class=\"o\">=</span> <span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">Output</span><span class=\"o\">.</span><span class=\"n\">concat</span><span class=\"p\">(</span><span class=\"s2\">\"http://\"</span><span class=\"p\">,</span> <span class=\"n\">hermes</span><span class=\"o\">.</span><span class=\"n\">slug</span><span class=\"p\">,</span> <span class=\"s2\">\":8642/v1\"</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Open WebUI: also a PRIVATE Render service, but the container joins your tailnet</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># (tailscaled in userspace + `tailscale serve`), so you reach it at</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># https://open-webui.<tailnet>.ts.net from your devices. Never public.</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># ---------------------------------------------------------------------------</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">webui</span> <span class=\"o\">=</span> <span class=\"n\">render</span><span class=\"o\">.</span><span class=\"n\">PrivateService</span><span class=\"p\">(</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"open-webui\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">name</span><span class=\"o\">=</span><span class=\"s2\">\"open-webui\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">plan</span><span class=\"o\">=</span><span class=\"s2\">\"standard\"</span><span class=\"p\">,</span> <span class=\"c1\"># 2 GB; Open WebUI OOMs on starter's 512 MB</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">region</span><span class=\"o\">=</span><span class=\"n\">region</span><span class=\"p\">,</span> <span class=\"c1\"># must match Hermes for private networking</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">runtime_source</span><span class=\"o\">=</span><span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"image\"</span><span class=\"p\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"image_url\"</span><span class=\"p\">:</span> <span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">Output</span><span class=\"o\">.</span><span class=\"n\">concat</span><span class=\"p\">(</span><span class=\"s2\">\"ghcr.io/\"</span><span class=\"p\">,</span> <span class=\"n\">ghcr_user</span><span class=\"p\">,</span> <span class=\"s2\">\"/open-webui\"</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"digest\"</span><span class=\"p\">:</span> <span class=\"n\">webui_image</span><span class=\"o\">.</span><span class=\"n\">digest</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"registry_credential_id\"</span><span class=\"p\">:</span> <span class=\"n\">ghcr_credential</span><span class=\"o\">.</span><span class=\"n\">id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">disk</span><span class=\"o\">=</span><span class=\"p\">{</span><span class=\"s2\">\"name\"</span><span class=\"p\">:</span> <span class=\"s2\">\"openwebui-data\"</span><span class=\"p\">,</span> <span class=\"s2\">\"mount_path\"</span><span class=\"p\">:</span> <span class=\"s2\">\"/app/backend/data\"</span><span class=\"p\">,</span> <span class=\"s2\">\"size_gb\"</span><span class=\"p\">:</span> <span class=\"mi\">5</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">env_vars</span><span class=\"o\">=</span><span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"PORT\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"8080\"</span><span class=\"p\">},</span> <span class=\"c1\"># tailscale serve points at this; Open WebUI binds it</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"OPENAI_API_BASE_URL\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"n\">hermes_api_base_url</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"ENABLE_OPENAI_API\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"true\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"ENABLE_OLLAMA_API\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"false\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"DEFAULT_MODELS\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"n\">model_default</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"WEBUI_AUTH\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"true\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"WEBUI_URL\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"n\">webui_tailnet_url</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\"># Reached only over the tailnet, so the first sign-up is the admin.</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"ENABLE_LOGIN_FORM\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"true\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"ENABLE_SIGNUP\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"true\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"DEFAULT_USER_ROLE\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"admin\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\"># Always apply env config; otherwise Open WebUI persists the first run's config.</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"ENABLE_PERSISTENT_CONFIG\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"false\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\"># Don't load local embedding/RAG models (a big memory saver; the agent is the brain).</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"RAG_EMBEDDING_ENGINE\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"openai\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"BYPASS_EMBEDDING_AND_RETRIEVAL\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"true\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"OFFLINE_MODE\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"true\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"HF_HUB_OFFLINE\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"s2\">\"1\"</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\"># Secrets (encrypted at rest):</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"OPENAI_API_KEY\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"n\">shared_key</span><span class=\"o\">.</span><span class=\"n\">base64</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"WEBUI_SECRET_KEY\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"n\">webui_secret_key</span><span class=\"o\">.</span><span class=\"n\">base64</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"c1\"># Tailscale joins this container to your tailnet and serves Open WebUI on it.</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"TS_AUTHKEY\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"n\">tailscale_auth_key</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"TS_HOSTNAME\"</span><span class=\"p\">:</span> <span class=\"p\">{</span><span class=\"s2\">\"value\"</span><span class=\"p\">:</span> <span class=\"n\">webui_hostname</span><span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">opts</span><span class=\"o\">=</span><span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">ResourceOptions</span><span class=\"p\">(</span><span class=\"n\">depends_on</span><span class=\"o\">=</span><span class=\"p\">[</span><span class=\"n\">hermes</span><span class=\"p\">],</span> <span class=\"n\">provider</span><span class=\"o\">=</span><span class=\"n\">render_provider</span><span class=\"p\">),</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">export</span><span class=\"p\">(</span><span class=\"s2\">\"hermesInternalUrl\"</span><span class=\"p\">,</span> <span class=\"n\">hermes_api_base_url</span><span class=\"p\">)</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Open this from any device on your tailnet:</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">pulumi</span><span class=\"o\">.</span><span class=\"n\">export</span><span class=\"p\">(</span><span class=\"s2\">\"openWebUiTailnetUrl\"</span><span class=\"p\">,</span> <span class=\"n\">webui_tailnet_url</span><span class=\"p\">)</span>\n</span></span>\n```\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">hermes-agent-pulumi</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">runtime</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">yaml</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">description</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">Deploy a Hermes agent (Render + Modal + Tailscale) as one declarative Pulumi program.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c\"># The Render SDK is bridged from the official Terraform provider. Run once:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c\"># pulumi package add terraform-provider render-oss/render 1.8.0</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c\"># Then `pulumi install` regenerates it on a fresh checkout.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">packages</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">render</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">source</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">terraform-provider</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">version</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">1.1.4</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">parameters</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"l\">render-oss/render</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"m\">1.8.0</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">config</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">renderRegion</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">string</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">default</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">oregon</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">modelDefault</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">string</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">default</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">gpt-5.5</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ghcrUser</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">string</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ghcrToken</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">string</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">secret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kc\">true</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">modalTokenId</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">string</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">secret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kc\">true</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">modalTokenSecret</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">string</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">secret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kc\">true</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">openrouterApiKey</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">string</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">secret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kc\">true</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">tailscaleAuthKey</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">string</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">secret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kc\">true</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">tailnetDnsName</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">string</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">renderApiKey</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">string</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">secret</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kc\">true</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">renderOwnerId</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">string</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">variables</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">registryAuth</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"nt\">address</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">ghcr.io</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">username</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${ghcrUser}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">password</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${ghcrToken}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Open WebUI reaches Hermes at its private slug; you reach Open WebUI on the tailnet.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">hermesApiBaseUrl</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">http://${hermes-agent.slug}:8642/v1</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">webuiTailnetUrl</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">https://open-webui.${tailnetDnsName}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">resources</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Render provider auth (from config or ESC).</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">render</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">pulumi:providers:render</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">properties</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">apiKey</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${renderApiKey}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ownerId</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${renderOwnerId}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Secrets generated in state: the replacement for `openssl rand -base64 32`.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">hermes-shared-key</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">random:RandomBytes</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">properties</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">length</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">32</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">webui-secret-key</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">random:RandomBytes</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">properties</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">length</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">32</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Build + push both images during `pulumi up` (no docker CLI).</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">hermes-image</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">docker-build:Image</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">properties</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">context</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">location</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">./hermes</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">dockerfile</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">location</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">./hermes/Dockerfile</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">platforms</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"p\">[</span><span class=\"l\">linux/amd64]</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">tags</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"l\">ghcr.io/${ghcrUser}/hermes-agent:latest</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">push</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kc\">true</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">registries</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${registryAuth}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">webui-image</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">docker-build:Image</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">properties</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">context</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">location</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">./openwebui</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">dockerfile</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">location</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">./openwebui/Dockerfile</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">platforms</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"p\">[</span><span class=\"l\">linux/amd64]</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">tags</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"l\">ghcr.io/${ghcrUser}/open-webui:latest</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">push</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kc\">true</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">registries</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${registryAuth}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Credential Render uses to pull the private GHCR images.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ghcr</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">render:RegistryCredential</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">properties</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">ghcr</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">registry</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">GITHUB</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">username</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${ghcrUser}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">authToken</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${ghcrToken}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">options</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">provider</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${render}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Hermes: a PRIVATE Render service. No public URL; encrypted env vars are the secret mechanism.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">hermes-agent</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">render:PrivateService</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">properties</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">hermes-agent</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">plan</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">standard </span><span class=\"w\"> </span><span class=\"c\"># 2 GB; the gateway OOMs on starter's 512 MB</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">region</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${renderRegion}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">runtimeSource</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">image</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">imageUrl</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">ghcr.io/${ghcrUser}/hermes-agent</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">digest</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${hermes-image.digest}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">registryCredentialId</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${ghcr.id}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">disk</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">hermes-data</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">mountPath</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">/opt/data</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">sizeGb</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">5</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">envVars</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">PORT</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"8642\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">API_SERVER_ENABLED</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"true\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">API_SERVER_HOST</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"0.0.0.0\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">API_SERVER_PORT</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"8642\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">API_SERVER_MODEL_NAME</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"${modelDefault}\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">HERMES_ACCEPT_HOOKS</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"1\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">API_SERVER_KEY</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"${hermes-shared-key.base64}\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">OPENROUTER_API_KEY</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"${openrouterApiKey}\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">MODAL_TOKEN_ID</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"${modalTokenId}\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">MODAL_TOKEN_SECRET</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"${modalTokenSecret}\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">options</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">provider</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${render}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Open WebUI: also a PRIVATE Render service, joined to your tailnet by its image.</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">open-webui</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">type</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">render:PrivateService</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">properties</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">open-webui</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">plan</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">standard </span><span class=\"w\"> </span><span class=\"c\"># 2 GB; Open WebUI OOMs on starter's 512 MB</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">region</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${renderRegion} </span><span class=\"w\"> </span><span class=\"c\"># must match Hermes for private networking</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">runtimeSource</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">image</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">imageUrl</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">ghcr.io/${ghcrUser}/open-webui</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">digest</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${webui-image.digest}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">registryCredentialId</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${ghcr.id}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">disk</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">name</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">openwebui-data</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">mountPath</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">/app/backend/data</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">sizeGb</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"m\">5</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">envVars</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">PORT</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"8080\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">OPENAI_API_BASE_URL</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"${hermesApiBaseUrl}\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ENABLE_OPENAI_API</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"true\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ENABLE_OLLAMA_API</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"false\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">DEFAULT_MODELS</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"${modelDefault}\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">WEBUI_AUTH</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"true\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">WEBUI_URL</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"${webuiTailnetUrl}\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ENABLE_LOGIN_FORM</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"true\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ENABLE_SIGNUP</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"true\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">DEFAULT_USER_ROLE</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"admin\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">ENABLE_PERSISTENT_CONFIG</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"false\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">RAG_EMBEDDING_ENGINE</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"openai\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">BYPASS_EMBEDDING_AND_RETRIEVAL</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"true\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">OFFLINE_MODE</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"true\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">HF_HUB_OFFLINE</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"1\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">OPENAI_API_KEY</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"${hermes-shared-key.base64}\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">WEBUI_SECRET_KEY</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"${webui-secret-key.base64}\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">TS_AUTHKEY</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"${tailscaleAuthKey}\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">TS_HOSTNAME</span><span class=\"p\">:</span><span class=\"w\"> </span>{<span class=\"w\"> </span><span class=\"nt\">value</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"s2\">\"open-webui\"</span><span class=\"w\"> </span>}<span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">options</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">provider</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${render}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">dependsOn</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span>- <span class=\"l\">${hermes-agent}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nt\">outputs</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">hermesInternalUrl</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${hermesApiBaseUrl}</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"c\"># Open this from any device on your tailnet:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">openWebUiTailnetUrl</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"l\">${webuiTailnetUrl}</span><span class=\"w\">\n</span></span></span>\n```\n\nNeither box is published to the internet. The same code, plus the Dockerfiles and the Tailscale entrypoint, is in the companion repo:\n\n[![GitHub repository: dirien/hermes-agent-pulumi](https://opengraph.githubassets.com/1/dirien/hermes-agent-pulumi)\n\ngithub.com/dirien/hermes-agent-pulumi\n\n](https://github.com/dirien/hermes-agent-pulumi)\n\n## Cost\n\nTwo details shape the bill. The host starts on Render’s free [Hobby workspace](https://render.com/pricing), but a private service with a persistent disk runs on paid compute, so you need a card on file the moment you add them. There is no free-tier version of this architecture. Both services also need the 2 GB Standard plan, since the gateway and Open WebUI each OOM on the 512 MB Starter.\n\nHermes gateway\n\nOpen WebUI\n\n**Plan**\n\nStandard (2 GB)\n\nStandard (2 GB)\n\n**Compute**\n\n~$25/mo\n\n~$25/mo\n\n**Disk**\n\n5 GB\n\n5 GB\n\n**Public URL**\n\nnone\n\nnone (tailnet only)\n\nThat lands the host around $50/month, in line with what a comparable managed setup runs. [Modal](https://modal.com/pricing) and [OpenRouter](https://openrouter.ai/) bill by usage on top, so the total tracks how hard the agent works rather than a flat number. Tailscale is free for a personal tailnet, so the private gateway adds nothing to that.\n\nThe [persistent disk](https://render.com/docs/disks) pins each service to a single instance, so there is no autoscaling, and a redeploy drops a few seconds of connectivity while the old instance lets go of the disk. For a single-user agent that is a fine trade, and it is why “stand it up in a second region” is a copy-the-config move and not a live failover.\n\n![OpenRouter dashboard showing model usage and remaining credit](screenshot-openrouter-dashboard.png)\n\n*The OpenRouter Activity dashboard: 56 requests and 738K tokens of `gpt-5.5` usage, all routed through one key.*\n\n## Running the deployment\n\nWith your ESC environment referenced in `Pulumi.dev.yaml`, deploy with:\n\n```bash\n<span class=\"line\"><span class=\"cl\">pulumi up\n</span></span>\n```\n\nThis generates the Render SDK’s resources, builds and pushes both images through buildx, and creates the two private services with their disks and encrypted env vars, all in one run, with no `docker` CLI and no secret import hiding in a script. When it finishes, both services show up in the Render dashboard with no public URL.\n\n![Render dashboard showing the hermes-agent and open-webui private services](screenshot-render-services.png)\n\n*The `hermes-agent` and `open-webui` private services in the Render dashboard, both deployed with no public URL.*\n\n### Access the agent\n\nGive it a few minutes after `pulumi up` finishes. The images still have to be pulled and the containers booted, and Open WebUI has to join your tailnet before the URL resolves. Refresh until it loads.\n\nOpen WebUI is reachable only from a device on your tailnet, at `https://open-webui.<your-tailnet>.ts.net`, with no domain to buy, no DNS to point, and no certificate to manage. Open it, sign up so the first account becomes the admin, and ask the agent to run a Python command. It should fire up a Modal sandbox and return the result, with nothing exposed to the public internet at any point.\n\n![Open WebUI answering a code prompt by running it in a Modal sandbox](screenshot-openwebui-test.png)\n\n*Open WebUI on the tailnet URL: the `gpt-5.5` agent runs `sum(i*i for i in range(1, 1001))` in a Modal sandbox and returns `333833500`.*\n\nWhen the agent runs that code, Modal builds a throwaway sandbox specifically for it. There is no Modal resource to declare, no Modal CLI to call, and no Modal provider to bridge. The Hermes service carries `MODAL_TOKEN_ID` and `MODAL_TOKEN_SECRET` as two encrypted env vars, and that is the whole footprint.\n\n![Modal dashboard showing the per-task sandboxes the agent created at runtime](screenshot-modal-sandboxes.png)\n\n*The Modal dashboard: the per-task sandboxes the Hermes agent spun up at runtime to execute code.*\n\n### Verify the deployment (optional)\n\nIf the chat UI does not load, give the services a few minutes to finish their first boot, then check two things:\n\n1. **Render dashboard logs** for the `hermes-agent` and `open-webui` services, to confirm both started and pulled their images.\n2. **Tailscale admin console** ([Machines](https://login.tailscale.com/admin/machines)), to confirm the `open-webui` node joined your tailnet. If it did not, the `TS_AUTHKEY` is likely expired or not reusable.\n\n## Security considerations\n\nSelf-hosted AI servers get found fast: scanners like [Shodan](https://www.shodan.io/) and [Censys](https://censys.io/) enumerate a freshly exposed one within hours to days, and the code-capable ones are already under [active attack](https://www.pillar.security/blog/operation-bizarre-bazaar-first-attributed-llmjacking-campaign-with-commercial-marketplace-monetization). Running an always-on agent on a machine you also use for everything else invites prompt injection on top of that. The answer here is to keep the agent unroutable from the internet and to isolate the code it runs. That defeats the scanner: nobody finds an open port to attack. It does less against the agent being turned against you from the inside, which is the harder problem.\n\nOne detail makes that isolation essential. In a headless container there is no one at the terminal to approve anything, and the same is true of its cron and CI runs, so the deployment runs Hermes with `HERMES_ACCEPT_HOOKS=1` to [auto-accept the shell hooks](https://hermes-agent.nousresearch.com/docs/user-guide/features/hooks) it would otherwise pause on. The practical result is that the agent’s code runs with no human in the loop, which is exactly why the Modal sandbox has to carry the weight: when the model writes a bad command, the throwaway container it runs in is what stands between that command and the gateway’s credentials, not a confirmation prompt.\n\nConcern\n\nTypical self-host\n\nThis deployment\n\nGateway reachability\n\nPublic port, or behind a login\n\nPrivate service, no public URL\n\nChat UI reachability\n\nPublic hostname + auth\n\nTailnet only (`tailscale serve`)\n\nCode execution\n\nRuns next to the gateway\n\nIsolated Modal sandbox, torn down after\n\nCommand approval\n\nHuman approves dangerous commands\n\nHeadless: isolation replaces the prompt\n\nSecrets in transit\n\nExposed if accessed over HTTP\n\nEncrypted env vars; tailnet-encrypted access\n\nThis is private, which is not the same as fully trusted. A few places the boundary still leaks:\n\n- **The shared bearer never rotates.** Hermes is unroutable from the internet, but it is wide open to Open WebUI, and the two share one static bearer key. Whoever owns the front end owns the gateway behind it. Rotate it periodically.\n- **The tailnet policy is click-ops.** The ACLs, the node’s tags, and the one-time HTTPS toggle live in the Tailscale admin console, not in `pulumi up`, and the reusable key has no tag scoping its blast radius. The Tailscale provider can manage that policy declaratively if you want it in code.\n- **Signup defaults to admin.** Fine on a single-user tailnet, the first thing to lock down (`ENABLE_SIGNUP`, `DEFAULT_USER_ROLE`) once more than one person can reach the URL.\n- **Every messaging client is another entry point.** Each platform you wire up adds another way in, which is why the tailnet is the baseline for securing this, not the full extent of it.\n- **A compromised agent can still reach out.** The private core stops external discovery, but it does not contain the agent itself. The gateway holds the Modal and OpenRouter tokens as env vars and can call out freely, so a prompt-injected agent could read its own environment and send them somewhere; the Modal sandbox isolates the code the agent runs, not the credentials it holds. Cap and rotate the [OpenRouter key](https://openrouter.ai/docs/cookbook/administration/api-key-rotation), keep the Modal token off the gateway, and allowlist outbound traffic, which on Render means a forward-proxy sidecar (Render gives you [dedicated outbound IPs](https://render.com/docs/outbound-ip-addresses) and inbound rules, but no destination egress filter).\n\nMy recommendations:\n\n- Keep both services private; never give Hermes a public URL\n- Run code in Modal sandboxes, since `HERMES_ACCEPT_HOOKS=1` means no human approves commands\n- Rotate the shared bearer key and the Tailscale auth key periodically\n- Cap and rotate the OpenRouter key; a leaked credential is the likelier failure mode than an open port\n- Use Pulumi ESC for secrets instead of hardcoding\n- Lock down Open WebUI signup once more than one person can reach it\n- Manage Tailscale ACLs and tags deliberately, in the admin console or with the Tailscale provider\n\n## What’s next?\n\nThe deployment is the foundation the agent builds on. A private agent becomes useful when it remembers earlier context, runs code without a human at the terminal, and initiates contact on its own. The disk is what makes this persist: its memory and its schedule live on that disk, so they survive every restart.\n\n**It schedules itself.** Hermes ships a first-class [cron](https://hermes-agent.nousresearch.com/docs/user-guide/features/cron) you drive in plain language, either by asking the agent in the chat window or through its CLI. The schedule takes intervals, five-field cron, or one-shots:\n\n```bash\n<span class=\"line\"><span class=\"cl\">hermes cron create <span class=\"s2\">\"every 1h\"</span> <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> <span class=\"s2\">\"Scrape <listing-url> for the price, compare it with seen.json in the sandbox, message me on Telegram only if it dropped, and otherwise reply exactly [SILENT].\"</span> <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> --deliver telegram\n</span></span>\n```\n\nThe `[SILENT]` reply suppresses delivery when nothing changed, so the watcher only speaks up when there is something to say. Scheduled runs skip memory by default, so polling does not pollute what the agent has learned.\n\n**It connects where you work, with the same secret pattern.** Messaging clients attach to the same gateway, and their tokens ride as encrypted env vars on the Hermes service, the exact shape you used for the Modal pair:\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// alongside MODAL_TOKEN_ID on the hermes service:\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"nx\">TELEGRAM_BOT_TOKEN</span><span class=\"o\">:</span> <span class=\"p\">{</span> <span class=\"nx\">value</span>: <span class=\"kt\">cfg.requireSecret</span><span class=\"p\">(</span><span class=\"s2\">\"telegramBotToken\"</span><span class=\"p\">)</span> <span class=\"p\">},</span>\n</span></span>\n```\n\nplus a platform block you add to `config.yaml` (the image ships only the model and terminal blocks; Hermes supports the messaging platforms on top, per its docs):\n\n```yaml\n<span class=\"line\"><span class=\"cl\"><span class=\"nt\">gateway</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">platforms</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">telegram</span><span class=\"p\">:</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"nt\">require_mention</span><span class=\"p\">:</span><span class=\"w\"> </span><span class=\"kc\">true</span><span class=\"w\">\n</span></span></span>\n```\n\nWith those two pieces, a few things are genuinely doable today:\n\n1. **A price or uptime watcher** that only speaks up when something changed, using the `[SILENT]` and persistent-sandbox dedup pattern above.\n2. **A daily briefing** that ranks and dedups your feeds and delivers the top items each morning, the documented [daily-briefing bot](https://hermes-agent.nousresearch.com/docs/guides/daily-briefing-bot). If you want it to adapt to your taste over time, that is the optional [Honcho](https://hermes-agent.nousresearch.com/docs/user-guide/features/honcho) memory provider’s job; it reads your preferences out of the conversation itself. The built-in file memory has no version of that.\n3. **A PR-review teammate** that reviews open pull requests against your repo’s `AGENTS.md` conventions and saves recurring patterns as a skill, so the fifth review needs less hand-holding than the first.\n\nA few limits are worth stating plainly, since demos tend to overstate the capabilities. Hermes does not ship Gmail, Calendar, or market integrations out of the box; those are wired up as [MCP servers](https://modelcontextprotocol.io/) or skills, which is more work than a launch video implies. It curates its own memory and improves its own skills, but it does not retrain the underlying model or autonomously change its high-level strategy, so “self-learning” here means whatever it writes into memory and skills each run.\n\n## Conclusion\n\nDeploying a Hermes agent with infrastructure as code means you can reproduce the setup anytime, version control it, and tear it down with a single `pulumi destroy`. Keeping both services private, with no public URL and no exposed ports, means the code-executing core never sits on the internet for someone to find.\n\nThe part worth carrying to the next service you deploy: not every layer has a provider, and that is fine. The task is to tell a real resource apart from a runtime dependency wearing a costume. Render and the images are resources you declare; Modal and Tailscale are credentials you hand over. Draw that map first, and the program follows from it.\n\nTo generate that map instead of writing it by hand, [Pulumi Neo](https://www.pulumi.com/product/neo/) can take a target architecture like this one and produce a first draft of the program. The agent side of this shift is covered in [How Building AI Agents Has Changed in 2026](https://www.pulumi.com/blog/how-building-ai-agents-has-changed/) and the platform side in [Agent Sprawl Is Here. Your IaC Platform Is the Answer.](https://www.pulumi.com/blog/agent-sprawl-iac-platform-is-the-answer/).\n\nIf you run into issues or have questions, drop by the [Pulumi Community Slack](https://slack.pulumi.com/) or [GitHub Discussions](https://github.com/pulumi/pulumi/discussions). New to Pulumi? [Get started here](https://www.pulumi.com/docs/get-started/).","publishedAt":"2026-06-24T00:00:00.000Z","fetchedAt":"2026-06-24T12:00:36.165Z","url":"https://www.pulumi.com/blog/deploy-a-hermes-agent-with-pulumi/","media":[{"type":"image","url":"https://www.pulumi.com/blog/deploy-a-hermes-agent-with-pulumi/meta.png","r2Key":"releases/f19ce60c3d560fe7a5a58e78265574ee7d170d043c05d712c249c3f64dbd41a3.png","r2Url":"https://media.releases.sh/releases/f19ce60c3d560fe7a5a58e78265574ee7d170d043c05d712c249c3f64dbd41a3.png"},{"type":"image","url":"https://opengraph.githubassets.com/1/dirien/hermes-agent-pulumi","alt":"GitHub repository: dirien/hermes-agent-pulumi","r2Key":"releases/c52467aad82cf1bb1b4963ef54faccca83b326c9ed35ee1761db4c9376df2ea9.png","r2Url":"https://media.releases.sh/releases/c52467aad82cf1bb1b4963ef54faccca83b326c9ed35ee1761db4c9376df2ea9.png"}],"coverageCount":0},{"id":"rel_DmLVCA9pL47I3GKyDc7i3","version":null,"type":"feature","title":"Cloudflare-First Networking as Code with Pulumi","summary":"New blog post walks through building a standardized Cloudflare edge baseline with Pulumi, covering DNS records, WAF custom rules, a Worker canary, and Zero Trust Access policies.","titleGenerated":"Pulumi blog walks through Cloudflare edge infrastructure as code","titleShort":"Cloudflare edge infrastructure defined with Pulumi code","breaking":"none","importance":null,"content":"![](https://www.pulumi.com/blog/cloudflare-first-networking-with-pulumi/meta.png)\n\nPlatform teams managing multi-cloud applications face a dangerous visibility gap. While origin infrastructure is tightly controlled, the edge configuration often drifts through manual console tweaks. DNS records point to stale origins, WAF rules are inconsistent across environments, and Zero Trust policies fail to keep pace with team changes. This edge drift leads to application exposure or routing failures that origin teams only notice after users report them.\n\nAs applications span multiple clouds, the edge often becomes the most consistent layer for enforcing security and traffic policies. The cost of waiting for a unified edge strategy is high. Every manual change is a potential security hole or a performance bottleneck that bypasses your standard CI/CD rigor.\n\n## What you’ll build\n\nIn this post, you will build a standardized Cloudflare edge baseline. You’ll use Pulumi to define:\n\n1. **DNS records** to point to your multi-cloud origins.\n2. **WAF custom rules** to block malicious traffic before it hits your network.\n3. **A Worker canary** to handle edge logic and header validation.\n4. **Zero Trust Access policies** to secure internal tools and origin access.\n\nBy the end, you will have a version-controlled edge that you can validate and deploy across any environment.\n\n## The Pulumi program\n\nWe’ll build a Pulumi program that sets up a complete edge environment. This approach ensures that your edge security and routing are always in sync with your application code.\n\n### Setting up the provider\n\nFirst, ensure you have the `@pulumi/cloudflare` package installed and your credentials configured. The examples below are checked against `@pulumi/cloudflare@6.15.0`, the current v6 Pulumi Registry API at the time of writing.\n\n```bash\n<span class=\"line\"><span class=\"cl\">npm install @pulumi/cloudflare@6.15.0\n</span></span><span class=\"line\"><span class=\"cl\">pulumi config <span class=\"nb\">set</span> --secret cloudflare:apiToken <span class=\"s2\">\"</span><span class=\"nv\">$CLOUDFLARE_API_TOKEN</span><span class=\"s2\">\"</span>\n</span></span>\n```\n\n### Defining the infrastructure\n\nHere is the complete Pulumi program in TypeScript. If your zone already has a custom WAF ruleset for the same phase, import that ruleset or add the rule to your existing ruleset instead of creating another phase-level ruleset that may fail or conflict.\n\n```typescript\n<span class=\"line\"><span class=\"cl\"><span class=\"kr\">import</span> <span class=\"o\">*</span> <span class=\"kr\">as</span> <span class=\"nx\">cloudflare</span> <span class=\"kr\">from</span> <span class=\"s2\">\"@pulumi/cloudflare\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">accountId</span> <span class=\"o\">=</span> <span class=\"s2\">\"<your-cloudflare-account-id>\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">trustedAdminIp</span> <span class=\"o\">=</span> <span class=\"s2\">\"198.51.100.10\"</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">zone</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">cloudflare</span><span class=\"p\">.</span><span class=\"nx\">Zone</span><span class=\"p\">(</span><span class=\"s2\">\"my-zone\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">account</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">id</span>: <span class=\"kt\">accountId</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"example.com\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">www</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">cloudflare</span><span class=\"p\">.</span><span class=\"nx\">DnsRecord</span><span class=\"p\">(</span><span class=\"s2\">\"www\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">zoneId</span>: <span class=\"kt\">zone.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"www\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">content</span><span class=\"o\">:</span> <span class=\"s2\">\"1.2.3.4\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ttl</span>: <span class=\"kt\">1</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">type</span><span class=\"o\">:</span> <span class=\"s2\">\"A\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">proxied</span>: <span class=\"kt\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">admin</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">cloudflare</span><span class=\"p\">.</span><span class=\"nx\">DnsRecord</span><span class=\"p\">(</span><span class=\"s2\">\"admin\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">zoneId</span>: <span class=\"kt\">zone.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"admin\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">content</span><span class=\"o\">:</span> <span class=\"s2\">\"1.2.3.4\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">ttl</span>: <span class=\"kt\">1</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">type</span><span class=\"o\">:</span> <span class=\"s2\">\"A\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">proxied</span>: <span class=\"kt\">true</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">wafRule</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">cloudflare</span><span class=\"p\">.</span><span class=\"nx\">Ruleset</span><span class=\"p\">(</span><span class=\"s2\">\"waf-rule\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">zoneId</span>: <span class=\"kt\">zone.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Block suspicious traffic\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">description</span><span class=\"o\">:</span> <span class=\"s2\">\"Custom WAF rule to block specific patterns\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">kind</span><span class=\"o\">:</span> <span class=\"s2\">\"zone\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">phase</span><span class=\"o\">:</span> <span class=\"s2\">\"http_request_firewall_custom\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">rules</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">action</span><span class=\"o\">:</span> <span class=\"s2\">\"block\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">expression</span><span class=\"o\">:</span> <span class=\"sb\">`(http.request.uri.path contains \"/admin\" and not ip.src in {</span><span class=\"si\">${</span><span class=\"nx\">trustedAdminIp</span><span class=\"si\">}</span><span class=\"sb\">})`</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">description</span><span class=\"o\">:</span> <span class=\"s2\">\"Block unauthorized admin access\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">worker</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">cloudflare</span><span class=\"p\">.</span><span class=\"nx\">WorkersScript</span><span class=\"p\">(</span><span class=\"s2\">\"canary-worker\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">accountId</span>: <span class=\"kt\">accountId</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">scriptName</span><span class=\"o\">:</span> <span class=\"s2\">\"edge-canary\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">compatibilityDate</span><span class=\"o\">:</span> <span class=\"s2\">\"2026-01-01\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">mainModule</span><span class=\"o\">:</span> <span class=\"s2\">\"worker.js\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">content</span><span class=\"o\">:</span> <span class=\"sb\">`\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sb\"> export default {\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sb\"> async fetch(request) {\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sb\"> return new Response(\"Edge Baseline Validated\", {\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sb\"> headers: { \"x-edge-baseline\": \"active\" },\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sb\"> });\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sb\"> },\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sb\"> };\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"sb\"> `</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">workerRoute</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">cloudflare</span><span class=\"p\">.</span><span class=\"nx\">WorkersRoute</span><span class=\"p\">(</span><span class=\"s2\">\"canary-route\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">zoneId</span>: <span class=\"kt\">zone.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">pattern</span><span class=\"o\">:</span> <span class=\"s2\">\"www.example.com/*\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">script</span>: <span class=\"kt\">worker.scriptName</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">adminGroup</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">cloudflare</span><span class=\"p\">.</span><span class=\"nx\">ZeroTrustAccessGroup</span><span class=\"p\">(</span><span class=\"s2\">\"admin-group\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">accountId</span>: <span class=\"kt\">accountId</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Administrators\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">includes</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">email</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">email</span><span class=\"o\">:</span> <span class=\"s2\">\"admin@example.com\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kr\">const</span> <span class=\"nx\">accessApp</span> <span class=\"o\">=</span> <span class=\"k\">new</span> <span class=\"nx\">cloudflare</span><span class=\"p\">.</span><span class=\"nx\">ZeroTrustAccessApplication</span><span class=\"p\">(</span><span class=\"s2\">\"internal-tool\"</span><span class=\"p\">,</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">accountId</span>: <span class=\"kt\">accountId</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Internal Admin Tool\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">domain</span><span class=\"o\">:</span> <span class=\"s2\">\"admin.example.com\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"kr\">type</span><span class=\"o\">:</span> <span class=\"s2\">\"self_hosted\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">policies</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">name</span><span class=\"o\">:</span> <span class=\"s2\">\"Allow administrators\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">precedence</span>: <span class=\"kt\">1</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">decision</span><span class=\"o\">:</span> <span class=\"s2\">\"allow\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">includes</span><span class=\"o\">:</span> <span class=\"p\">[{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">group</span><span class=\"o\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nx\">id</span>: <span class=\"kt\">adminGroup.id</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}],</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">});</span>\n</span></span>\n```\n\n## Validation\n\nOnce you run `pulumi up`, you can validate your edge baseline with these steps:\n\n1. **DNS check**: Run `dig www.example.com` to confirm the record points to Cloudflare’s proxied IPs.\n2. **WAF test**: Attempt to access `www.example.com/admin` from an unauthorized IP and verify you receive a 403 Forbidden response.\n3. **Worker canary**: Use `curl -I https://www.example.com` and check for the `x-edge-baseline: active` header.\n4. **Access policy**: Navigate to `admin.example.com` and verify you are redirected to the Cloudflare Access login page.\n\nBy managing these resources as code, you eliminate the risk of manual drift and ensure that every environment starts with a secure, validated edge baseline.","publishedAt":"2026-06-23T00:00:00.000Z","fetchedAt":"2026-06-23T11:01:32.827Z","url":"https://www.pulumi.com/blog/cloudflare-first-networking-with-pulumi/","media":[{"type":"image","url":"https://www.pulumi.com/blog/cloudflare-first-networking-with-pulumi/meta.png","r2Key":"releases/975a21f28aacf0e2aead59994f61a48fdf89ab77dbda7a4b8c67df3d7f50c49d.png","r2Url":"https://media.releases.sh/releases/975a21f28aacf0e2aead59994f61a48fdf89ab77dbda7a4b8c67df3d7f50c49d.png"}],"coverageCount":0},{"id":"rel_Ehw3pbtsE9hlVW_oGkpoL","version":null,"type":"feature","title":"Neo code reviews: AI code review built for infrastructure","summary":"Pulumi introduced Neo code reviews, now in public preview, which analyze pull request changes against live cloud infrastructure and preview output. Reviews post as comments on GitHub PRs, respect existing RBAC and guardrails, and are free during preview before GA on July 1, 2026.","titleGenerated":"Pulumi launches Neo code reviews for infrastructure-as-code pull requests","titleShort":"Neo code reviews analyze IaC against live infrastructure","breaking":"none","importance":null,"content":"![](https://www.pulumi.com/blog/neo-code-reviews/meta.png)\n\nToday we’re introducing [Pulumi Neo code reviews](https://www.pulumi.com/docs/ai/code-reviews/), now in public preview. Neo code reviews analyze pull request changes in conjunction with what Pulumi Cloud knows about your running infrastructure, providing both high-level and code-level feedback.\n\nNormal code review agents can’t reliably anticipate the impact an infrastructure-as-code change will have. This is because they don’t have access to critical aspects of the IaC workflow: the potential impact the update will have, in this case the `pulumi preview` output; and the current state of the cloud infrastructure. Neo not only has access to both of those, but also to the entirety of your other cloud context, such as stack relationships and dependencies.\n\n## Running reviews\n\nNeo can review every pull request automatically, or only when someone mentions `@pulumi-neo`. Either way, it skips draft pull requests and those opened by bots by default.\n\nA review is a comment, so it informs the person approving the merge and sits alongside the required checks and branch protection you already enforce. Neo code reviews run inside the same governance as every other Neo task, with the [RBAC](https://www.pulumi.com/docs/administration/access-identity/rbac/), guardrails, and audit logging your organization has set.\n\n![Neo code review on a GitHub pull request, showing a risk assessment and the resource changes the update would make](neo-code-review.png)\n\n## Enable code reviews\n\nNeo code reviews are available on GitHub during public preview. They require Pulumi Neo to be enabled for your organization, the [Pulumi GitHub App](https://www.pulumi.com/docs/integrations/version-control/github-app/) installed on the repositories you want reviewed, and a one-time grant from each organization user to access their GitHub account under **Management** > **Version control**. If Neo currently posts preview summaries on your pull requests, code reviews are already enabled, and they take the place of those summaries.\n\nNeo code reviews are free while in public preview. On July 1, 2026, they’ll be generally available, and reviews will begin counting toward your organization’s Neo token usage, at the same per-token rate as any other Neo task. The [pricing page](https://www.pulumi.com/pricing/) shows that rate and the monthly token allotment included with each plan.\n\n## Give it a try\n\nOpen a pull request against a stack Pulumi manages and see Neo’s review. We want to hear what it catches and what it misses, so hop into the [Pulumi Community Slack](https://slack.pulumi.com/) and tell us.","publishedAt":"2026-06-22T15:00:00.000Z","fetchedAt":"2026-06-23T01:01:24.349Z","url":"https://www.pulumi.com/blog/neo-code-reviews/","media":[{"type":"image","url":"https://www.pulumi.com/blog/neo-code-reviews/meta.png","r2Key":"releases/a7bf7dd791583d90f2fbd165b4b453b612ad4313586883f3a1f5fc169663bacf.png","r2Url":"https://media.releases.sh/releases/a7bf7dd791583d90f2fbd165b4b453b612ad4313586883f3a1f5fc169663bacf.png"}],"coverageCount":0},{"id":"rel_EzRN1Tn4cUbHyLrtAgjF_","version":null,"type":"feature","title":"Six Live Kubernetes Recommendations: AKS, Cilium, Rate Limiting, and More","summary":"Workshop writeup covers six recommendations for Kubernetes on Azure, including enabling Cilium on AKS, picking a team-aligned language, using AI-assisted debugging to route around Docker Hub rate limits, splitting infra from workload into stacks, and managing state with Pulumi Cloud.","titleGenerated":"Pulumi blog: AKS workshop yields six Kubernetes recommendations","titleShort":"AKS workshop: Cilium, rate limiting, and six recommendations","breaking":"unknown","importance":null,"content":"![](https://www.pulumi.com/blog/getting-started-aks-pulumi-csharp/meta.png)\n\n*On June 10th, Engin and I ran a live workshop building an AKS cluster, an Azure Container Registry, and a random-cat web app from scratch in C#. This is the writeup, including the parts we didn’t get to live.*\n\n[Video](https://www.youtube.com/watch?v=-K_lI65Djzs)\n\nLive demos keep you honest. On June 10th my AKS workshop went a little sideways. Partway through, Docker Hub rate-limited my image pull and we had to adapt the content on the fly. The original plan was to stand up an AKS cluster with Cilium, an Azure Container Registry with the cluster’s pull permission wired in code, and a random-cat web app, then split the infrastructure from the workload into separate Pulumi stacks. Live, we didn’t make it through all of that, but we had some fun tangents and it turned out to be a great session. So here are my six recommendations for working with Kubernetes on Azure from this recent workshop.\n\n## 1\\. Pick the language your team already uses\n\nI opened the session by asking the room what language they prefer to work in, with one caveat from me: “Hopefully, it’s not PowerShell because this is not in PowerShell.” We got some C# answers and zero requests for PowerShell, which works out, because the whole workshop is in PowerShell C#.\n\nI chose C# because, first, I genuinely love the language. I spent the early part of my career as a C# developer, and on Azure it just makes sense. Second, an Azure workshop tends to draw a big turnout of C# devs, and if they aren’t C# devs themselves, they often work somewhere the backend team uses C#. Pulumi lets you bring that language to your infrastructure rather than learn a new DSL.\n\nAI coding assistants also came up. LLMs have substantially more C# and Go in their training data than HCL or Bicep. As I put it live: “If you’re working in HCL, well, there’s not that much out there in the training set, but there’s lots of C#. There’s lots of Go.” I co-authored parts of the workshop code with Claude.\n\n**Pick the language your team already writes.**\n\nPulumi runs on the language your backend engineers know, and that language has more AI assistant training data behind it too.\n\n## Aside: everyone’s path ran through Docker Swarm\n\nWe also had some fun historical conversations about orchestrators. I asked Engin: “Did you use anything before Kubernetes became popular? Like Mesosphere or any of those other interesting solutions of the heyday?”\n\nEngin had walked the same road most of us did.\n\n> **Engin:** “We used the classical Ansible powered provisioning of VMs… And then we switched to Consul to have a little bit more control… And then we had a short intermezzo with Docker Swarm… So Docker Swarm with Portainer was really goated. And Kubernetes then took over.”\n\nMy own history rhymed with his.\n\n> **Adam:** “I worked at a place where we used a bunch of shell scripts to have like JVM services, like a red green deploy… And then it was Docker Swarm for a little bit. And then it was Kubernetes.”\n\nNo single tool won. Teams picked what fit their existing skills and shifted gradually as the ecosystem matured.\n\n## 2\\. Use Cilium on AKS if you can\n\nBy default, Kubernetes on AKS doesn’t turn on Cilium. My strong recommendation is to turn it on. The option is called Azure CNI Powered by Cilium, and it takes one config block:\n\n```csharp\n<span class=\"line\"><span class=\"cl\"><span class=\"n\">NetworkProfile</span> <span class=\"p\">=</span> <span class=\"k\">new</span> <span class=\"n\">ACI</span><span class=\"p\">.</span><span class=\"n\">ContainerServiceNetworkProfileArgs</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">NetworkDataplane</span> <span class=\"p\">=</span> <span class=\"s\">\"cilium\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">NetworkPlugin</span> <span class=\"p\">=</span> <span class=\"s\">\"azure\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">NetworkPluginMode</span> <span class=\"p\">=</span> <span class=\"s\">\"overlay\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">NetworkPolicy</span> <span class=\"p\">=</span> <span class=\"s\">\"cilium\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"n\">PodCidr</span> <span class=\"p\">=</span> <span class=\"s\">\"192.168.0.0/16\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">},</span>\n</span></span>\n```\n\nThis buys you eBPF networking instead of kube-proxy and iptables. [eBPF](https://ebpf.io/what-is-ebpf/) lets you run sandboxed programs inside the Linux kernel itself, so packet handling and `NetworkPolicy` enforcement happen down in the kernel instead of up in userspace. You also get Hubble flow visibility built in, the same technology as GKE’s Dataplane V2. As I said live, “this is very easy to set up on Azure. You just network data plane, set it to Cilium. Network policy, set it to Cilium.”\n\nSelf-hosted, Cilium is `helm install cilium` with a dozen flags and a lifecycle you own forever. On AKS, it’s these five lines and Azure runs it. On a getting-started cluster, I’ll take the managed version every time. Owning that lifecycle yourself buys you nothing here.\n\nFull code: [`01-cluster/Program.cs`](https://github.com/pulumi/workshops/blob/main/az-getting-started-aks/01-cluster/Program.cs)\n\n![Stage 1: cluster and ACR in place](01-cluster.png)\n\n**On managed Kubernetes, complexity you’d self-host becomes a field you set.**\n\nAKS gives you the managed version of eBPF networking. Pulumi makes the configuration code instead of portal clicks.\n\n## 3\\. Debugging Kubernetes is easier when your tools have the full context\n\nThe rate limit wasn’t even the first failure of the hour. That honor went to Azure, which refused to provision the cluster on the first `pulumi up`. I was running in Canada Central, and Pulumi Neo read the error and diagnosed it:\n\n> **Neo:** “Capacity constraints in the selected region. We might need to change our location.”\n> \n> **Adam:** “Maybe this Canadian Azure instance is not all it’s cracked up to be.”\n> \n> **Engin:** “East US is always a good bet.”\n\nWe switched the region to East US, and the cluster came up. That is the kind of diagnosis a stack-aware assistant is good at, reading the actual provisioning error instead of leaving me to guess.\n\nThen the image step hit. The workshop’s original flow pulled the cat app image into ACR with `az acr import --source docker.io/agbell/my-random-cat`, and Docker Hub rate-limited me, even though I was the only person in the room running the code. So how do you blow through an anonymous pull limit all by yourself? The trick is that `az acr import` runs server-side. Azure does the pull from Docker Hub, not my laptop, and it goes out over an egress IP shared by who-knows-how-many other Azure tenants. Docker Hub counts anonymous pulls per IP address, so all of that shared traffic lands in one bucket. It flagged me at the worst possible moment: “I just got flagged by Docker Hub. That is awesome. That will be a challenge for this demo.”\n\nNothing focuses a workshop like watching your own demo get rate-limited in front of a live audience.\n\nLive, I asked Neo to route around it: “we’re using Neo. And Neo is going to switch us out to a nice Hello World project.” Neo could see the whole stack, down to the failing image reference, and it swapped the workload over to a standard hello-world image hosted on Azure, so the deploy no longer depended on Docker Hub. A few minutes later: “we should have a Hello World container running here. It’s way less fun than my image… just a bunch of random cats. But here we can see that this is working.” I won’t oversell it. The cat was gone, and we didn’t get through every planned stage. But the cluster was up with a workload deployed, and the session kept moving.\n\nThat’s the case for a stack-aware assistant over a chatbot. A general chatbot can tell you that Docker Hub has rate limits and that you should host images closer to home. Neo was working with the actual program state, so the workaround it proposed was already wired to my resources. Engin put it this way: “for me, Pulumi Neo, the subcommand is the new Pulumi app.” When something breaks mid-demo, the tool that can read your stack is the one that helps.\n\nThe long-term fix was switching to `az acr build`, building the image in ACR from source, so the app image never has to come from Docker Hub at all. Every stage in the repo now builds the cat image this way, from source shipped in each stage’s `app/` folder:\n\n```bash\n<span class=\"line\"><span class=\"cl\">az acr build --registry <your-acr> --image my-random-cat:latest app\n</span></span>\n```\n\nNo local Docker, and the app image never touches Docker Hub. The only Docker Hub dependency left is the base image (`python:3.9-slim`), pulled server-side by ACR’s build instead of by an import you re-run every time.\n\nFull code: [`02-app/Program.cs`](https://github.com/pulumi/workshops/blob/main/az-getting-started-aks/02-app/Program.cs)\n\n![Stage 2: cluster, ACR, and cat app running](02-app.png)\n\n**An assistant that can read your stack beats one that can’t.**\n\nA general chatbot knows Docker Hub has rate limits. Neo could see the deployment and the failing image reference, and that context is what turned advice into a working fix mid-session.\n\n## 4\\. You don’t have to abandon Terraform to start\n\nOne attendee, Martin, had been using Terraform for eight-plus years. His question, put directly: “I’m using TF for eight plus years. Why should I consider Pulumi? Where is the strength?… It seems unnecessary to TF for me. At least from what I’m seeing now.”\n\nFair question, and the honest answer is you don’t have to abandon your modules. Pulumi has [first-class Terraform module support](https://www.pulumi.com/docs/iac/guides/building-extending/using-existing-tools/use-terraform-module/), so you can reference existing modules from a Pulumi program. Python ML teams can import infrastructure modules, and platform teams can wrap them in typed abstractions. The portfolio doesn’t change. The programs that use it just get more capable.\n\nThe sharper reason is about edges. HCL and Bicep are purpose-built DSLs, and when you hit what they can’t express, there’s no escape hatch inside the language. With a general-purpose language you can always drop to the SDK or shell out. The edges still exist. They’re just further out, and that’s the whole difference once a project gets past the basics.\n\n**You don’t have to rip and replace.**\n\nPulumi can reference existing Terraform modules. Move what makes sense now. The DSL/language tradeoff shows up at the edges, not the basics.\n\n## 5\\. Split slow infrastructure from fast workloads\n\nOnce we had the cluster up and the app running, the single program had grown: cluster, ACR, `AcrPull`, image build, deployment, service. Every new workload meant editing the file that also declares the cluster. We had planned this turn in the session in advance. I posed the problem and Engin had the recommendation ready:\n\n> **Adam:** “What do you think the problem is with my code here, Engin? … The problem is like every time I want to add a new service to my Kubernetes cluster, I have to go into this file that declares the cluster and all its infrastructure. I think I would prefer to split it.”\n> \n> **Engin:** “Create other stacks which are then referencing each other to get the values out. Actually, I highly recommend.”\n> \n> **Adam:** “Yes, I totally agree with you, Engin. We should split these out into two separate stacks.”\n\nSo that’s what we did. The infrastructure stack exports the kubeconfig and registry URL. The workload stack references those outputs via [`StackReference`](https://www.pulumi.com/docs/iac/concepts/stacks/#stackreferences). The slow-moving infra and the fast-moving app become independent deployment units. The infra team and the app team can ship on their own cadences, without fighting over one file.\n\n```csharp\n<span class=\"line\"><span class=\"cl\"><span class=\"c1\">// workload/Program.cs — pulls cluster outputs, never touches cluster resources</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kt\">var</span> <span class=\"n\">clusterStack</span> <span class=\"p\">=</span> <span class=\"k\">new</span> <span class=\"n\">StackReference</span><span class=\"p\">(</span><span class=\"n\">cfg</span><span class=\"p\">.</span><span class=\"n\">Require</span><span class=\"p\">(</span><span class=\"s\">\"clusterStack\"</span><span class=\"p\">));</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kt\">var</span> <span class=\"n\">kubeconfig</span> <span class=\"p\">=</span> <span class=\"n\">clusterStack</span><span class=\"p\">.</span><span class=\"n\">GetOutput</span><span class=\"p\">(</span><span class=\"s\">\"kubeconfig\"</span><span class=\"p\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"kt\">var</span> <span class=\"n\">acrLoginServer</span> <span class=\"p\">=</span> <span class=\"n\">clusterStack</span><span class=\"p\">.</span><span class=\"n\">GetOutput</span><span class=\"p\">(</span><span class=\"s\">\"acrLoginServer\"</span><span class=\"p\">);</span>\n</span></span>\n```\n\nThe tricky part is one we didn’t cover live, which is how you split a running workload without taking down the running services. The real answer is migrating the stack state, moving resources between stacks instead of destroying and recreating them. As I noted in the session: “now we are just removing the service from our project. If this was actual production stuff, I don’t think I’d be just shutting down the service… You can actually just make changes to the state rather than tearing them down.”\n\nFor the workshop, a simpler move kept the Kubernetes cluster up and the demo moving: keep the project name. Pulumi identifies stacks as `org/project/stack`, so if you move the cluster code into `aks-cluster/` while keeping the project name `kube-kitties`, Pulumi still sees `org/kube-kitties/dev`. Same stack, nothing recreated. Rename the project and you get a second cluster.\n\nIn stage four of the demo, I showed how to extend this pattern to work with existing YAML manifests: [`ConfigGroup`](https://www.pulumi.com/registry/packages/kubernetes/api-docs/yaml/configgroup/) drives raw Kubernetes YAML through the Pulumi provider, giving you previews and dependency ordering without rewriting files. One transformation swaps the Docker Hub image reference for the ACR copy, so manifests stay portable and pods pull from your private registry.\n\nCode: [`03-split/`](https://github.com/pulumi/workshops/tree/main/az-getting-started-aks/03-split) and [`04-split-yaml/`](https://github.com/pulumi/workshops/tree/main/az-getting-started-aks/04-split-yaml)\n\n![Stage 3: infra and workload as separate stacks, joined by a stack reference](03-split.png)\n\n![Stage 4: existing YAML manifests driven through Pulumi ConfigGroup](04-split-yaml.png)\n\n**Keep the project name when you split, and the cluster doesn’t move.**\n\nPulumi identifies stacks as `org/project/stack`. Same project name means the same stack. Rename it and you spin up a new cluster.\n\n## 6\\. GitOps is the way\n\nThe final stage of the workshop is left as homework, and it’s a great one to pick up. GitOps is the standard way to run Kubernetes at this point, the One True Way™ depending on who you ask. My position from the call: “I think I’m on team GitOps.”\n\nThe take-home is the [`05-gitops`](https://github.com/pulumi/workshops/tree/main/az-getting-started-aks/05-gitops) folder. We didn’t demo it live, but the code is real and verified: Pulumi stands up the cluster, installs Argo CD, and registers the cat as an Argo `Application`. From there, changes are `git push`.\n\nTo see Kubernetes and GitOps worked through end to end, watch the video above. Engin also recently ran a [GCP workshop](https://github.com/pulumi/workshops/tree/main/getting-started-with-kubernetes-google-cloud) with great examples of GitOps with Pulumi, using Flux.\n\n**Let Pulumi stand up the cluster, then let GitOps run the apps.**\n\nPulumi installs Argo CD or Flux as part of the infrastructure. From there, workload changes are a `git push`. Stage 5 of the workshop repo is a complete Argo CD example.\n\n## Try it yourself\n\nThat’s the whole workshop, six recommendations and two live failures that ended with a cat app running on AKS. To run it yourself, clone the repo. There’s a [DEMO.md](https://github.com/pulumi/workshops/blob/main/az-getting-started-aks/DEMO.md) with every step of this session, and the stage folders mirror the arc above. `01-cluster` is the infrastructure checkpoint, `02-app` adds the cat app built into your own registry, `03-split` and `04-split-yaml` separate infrastructure from workload, and `05-gitops` is the homework.\n\nPick a stage and bring it up:\n\n```bash\n<span class=\"line\"><span class=\"cl\">git clone https://github.com/pulumi/workshops\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"nb\">cd</span> workshops/az-getting-started-aks/02-app\n</span></span><span class=\"line\"><span class=\"cl\">pulumi up\n</span></span><span class=\"line\"><span class=\"cl\">curl http://<span class=\"k\">$(</span>pulumi stack output catServiceIp<span class=\"k\">)</span>/\n</span></span>\n```\n\nExpect 5 to 8 minutes waiting on cluster create. That’s just how long AKS takes. And since each folder is a complete, runnable checkpoint with its own stack, run `pulumi destroy` before moving to the next stage, or you’ll have several clusters billing at once.\n\nYou’ll need an Azure subscription, the Pulumi CLI, and the .NET SDK. Basic Kubernetes familiarity helps. For more on the building blocks, see the [Azure Native provider](https://www.pulumi.com/registry/packages/azure-native/) and the [Kubernetes provider](https://www.pulumi.com/registry/packages/kubernetes/) in the Pulumi Registry.\n\nThe full workshop code lives at [github.com/pulumi/workshops](https://github.com/pulumi/workshops) under `az-getting-started-aks`. Check out our upcoming sessions on the [workshop page](https://www.pulumi.com/events/), or subscribe to the newsletter to hear what’s coming up.\n\nIf there’s a seventh recommendation hiding in here, it’s the one I learned the hard way: never pull from Docker Hub live again. I’ll be keeping my images on a paid cloud registry from now on.\n\nEngin and I will be doing more of these, and we’d love to see you at the next one.\n\n*Parts of the workshop code were authored by Claude and Pulumi Neo, and parts of this writeup got help from both as well.*","publishedAt":"2026-06-15T12:00:00.000Z","fetchedAt":"2026-06-17T12:01:26.679Z","url":"https://www.pulumi.com/blog/getting-started-aks-pulumi-csharp/","media":[{"type":"image","url":"https://www.pulumi.com/blog/getting-started-aks-pulumi-csharp/meta.png","r2Key":"releases/e140673687135be09afc9c616ecbbe2d2c7ab4726b1472193f38517586db3e10.png","r2Url":"https://media.releases.sh/releases/e140673687135be09afc9c616ecbbe2d2c7ab4726b1472193f38517586db3e10.png"},{"type":"video","url":"https://www.youtube.com/watch?v=-K_lI65Djzs"},{"type":"video","url":"https://i.ytimg.com/vi/-K_lI65Djzs/hqdefault.jpg","alt":"Getting Started with Kubernetes on Azure (AKS) Using Pulumi C#","linkUrl":"https://www.youtube.com/watch?v=-K_lI65Djzs","r2Key":"releases/6a3d10e9be8e574177632a1f55e32e7f22bea082adbc496f2796b4fc18ac09ec.jpg","r2Url":"https://media.releases.sh/releases/6a3d10e9be8e574177632a1f55e32e7f22bea082adbc496f2796b4fc18ac09ec.jpg"}],"coverageCount":0},{"id":"rel_zb2wHHPDqg3E8A9-5SI8-","version":null,"type":"feature","title":"Stop Prompting. Design the Loop.","summary":"Article explores loop engineering, a shift from manually prompting coding agents turn-by-turn to designing automated systems that prompt agents continuously. Covers five building blocks (automations, worktrees, skills, connectors, sub-agents) plus memory infrastructure, and discusses verification, token costs, and knowledge maintenance as ongoing responsibilities.","titleGenerated":"Pulumi publishes guide to loop engineering for AI coding agents","titleShort":"Loop engineering replaces prompt-by-prompt agent work","breaking":"unknown","importance":null,"content":"![](https://www.pulumi.com/blog/stop-prompting-design-the-loop/meta.png)\n\nFor about two years, the unit of work with a coding agent was the prompt. You wrote a good one, you gave it enough context, you read what came back, and you wrote the next one. The agent was a tool, and you were holding it the entire time, one turn after another.\n\nThat part is ending. [Addy Osmani](https://x.com/addyosmani), a director of AI at Google Cloud, has a name for what replaces it, and I have not stopped thinking about it since: [loop engineering](https://x.com/addyosmani/status/2064127981161959567). You stop being the person who prompts the agent. You design the loop that prompts it for you.\n\nIn my phrasing: you stop being the thing that runs, and start designing the thing that runs. The leverage moves up a layer. What I want to do here is take an honest look at the pieces, and at the part nobody automates.\n\n## The leverage moved up a layer\n\nThe people building these tools have already made the jump. [Peter Steinberger](https://x.com/steipete) has been posting it as a monthly reminder.\n\n![A post by Peter Steinberger (@steipete) on X: 'Here's your monthly reminder that you shouldn't be prompting coding agents anymore. You should be designing loops that prompt your agents.'](./steinberger-loops.png)\n\n*Peter Steinberger ([@steipete](https://x.com/steipete)) on X.*\n\n[Boris Cherny](https://x.com/bcherny), who heads Claude Code at Anthropic, says the same thing about his own job. He does not prompt Claude anymore. He has loops running that prompt Claude and decide what to do next, scanning the issue tracker, the team chat, and the timeline for what to build. “My job is to write loops.”\n\nA loop is a goal that prompts itself. You set the purpose, and the system keeps iterating until it’s met. In practice it finds the work, hands it out, checks the result, writes down what got finished, and decides the next thing, then it pokes the agent instead of you. You build that small system once and let it run.\n\nLook closer, and a loop is really two loops nested. The inner one does the work against a spec. The outer one decides what the work should be: it watches an issue tracker, an error feed, a changelog, then writes the next spec and hands it down. Most people are still running that outer loop by hand, in their head, and calling it a backlog.\n\nThe part that surprised me is that this is barely a tooling problem anymore. A year ago a loop meant a pile of bash you wrote and maintained forever. Now the pieces ship inside the products, and the same shapes show up in Claude Code and in Codex. Osmani puts loop engineering one floor above the harness, the context and tooling you wire around a single agent. I wrote about [that harness](https://www.pulumi.com/blog/stop-tuning-prompts-build-a-harness/) a couple of weeks ago. The loop is the thing that runs on top of it: it runs on a timer, it spawns helpers, and it feeds itself.\n\n## The five pieces, and the one that holds them together\n\nStrip loop engineering down and you get roughly five building blocks, plus one place to remember things. Both Claude Code and Codex have all five now. The names differ here and there; the capability is the same.\n\n1. **Automations are the heartbeat.** They are what make a loop an actual loop and not one run you did once. A prompt or command on a cadence, a scheduled task, a hook that fires at a point in the agent’s lifecycle, or a job on CI that keeps running after you close the laptop. Discovery and triage run themselves, and the findings that matter come to you.\n2. **Worktrees keep parallel from turning into chaos.** The second you run more than one agent, the files start colliding. Two agents writing the same file is the same headache as two engineers committing to the same lines with nobody talking first. A git worktree is a separate working directory on its own branch, so one agent’s edits cannot touch another’s checkout.\n3. **Skills are intent, written down.** An agent starts every session cold and fills any hole in your intent with a confident guess. A skill is that intent written on the outside: the conventions, the build steps, the “we don’t do it like this because of that one incident,” recorded once where the agent reads it every run. Without skills, the loop re-derives your whole project from zero every cycle.\n4. **Connectors let the loop touch your real tools.** Built on MCP, they let the agent read the issue tracker, query a database, hit a staging API, or drop a message in chat. This is the difference between an agent that says “here is the fix” and a loop that opens the pull request, links the ticket, and pings the channel once CI goes green.\n5. **Sub-agents keep the maker away from the checker.** The model that wrote the code is far too generous grading its own homework. A second agent with different instructions, and sometimes a different model, catches the things the first one talked itself into. Worktrees and a cold-context reviewer are two pieces I have [written about before](https://www.pulumi.com/blog/parallel-coding-playbook-for-pulumi/), back when the question was running agents in parallel without them trampling each other.\n\nThen the sixth thing: memory. A markdown file, a Linear board, a state file, anything that lives outside the single conversation and holds what is done and what is next. It sounds too dumb to matter, and it’s the whole game. The model forgets everything between runs, so the memory has to live on disk, not in the context window. The agent forgets. The repo does not.\n\n## What makes a loop hold together\n\nA loop running unattended is also a loop making mistakes unattended. The one thing that keeps it honest is verification, and verification needs an oracle, something outside the model that returns a hard yes or no. Passing tests, a clean build, a green pipeline, a real production signal. Without an oracle, the loop compounds confidently wrong work, faster than you can read it.\n\nThe cleanest version of this already ships in the tools. Claude Code’s `/goal` keeps working across turns until a condition you actually wrote holds, something like “every test in `auth/` passes and lint is clean,” and after every turn a separate, faster model reads the transcript and decides whether you are there yet. The agent that wrote the code is not the one that grades it. That is the maker-and-checker split applied to the stop condition itself. Codex’s `/goal` reaches the same finish line a different way: the agent audits its own work against the evidence before it can call the goal done.\n\n## What the loop still won’t do for you\n\nThe loop changes the shape of the work. It does not take it off your desk. And a few things get sharper as the loop gets better, not softer.\n\n**Verification is still on you.** The split reviewer is what makes “it’s done” mean something, but “done” is a claim, not a proof. Your job is still to ship code you confirmed works, which is harder to remember when the diff arrived while you were at lunch.\n\n**The bill comes in two currencies.** Tokens and attention. A single unattended run can burn through millions of tokens, and that is only worth it when the tokens buy something worth more than they cost. The quieter trap is the second currency: memory is what lets a loop compound over time, and slop compounds right alongside it. A loop pointed at a vague goal does not get tired and stop. It gets faster.\n\n**Your understanding rots if you let it.** The faster the loop ships code you did not write, the wider the gap between what exists in the repo and what you actually understand. A smooth loop grows that gap faster, not slower, unless you read what it made. The comfortable posture, where you stop having an opinion and take whatever the loop gives back, is the risky one. Two engineers can build the exact same loop and get opposite results, one moving faster on work they understand deeply, the other avoiding the work entirely. The loop cannot tell which one you are.\n\n## When the loop reaches production\n\nMost of this thinking grew up around application code, where a bad run costs you a revert. When the loop reaches into infrastructure, the blast radius is a production outage rather than a revert, and the verification bar has to rise to meet it. The upside is that infrastructure hands the loop a better oracle than application code does. A plan diff is deterministic and machine-readable, a policy check returns a hard verdict, and drift and cost are numbers you can put a threshold on. A reviewer, whether human or agent, can read the change cold, with no memory of the prompt that produced it. That cold-context check is exactly what an unattended loop needs, and it’s the reason an infrastructure loop can be built to hold together while you sleep. [Pulumi Neo reasons over the state graph directly](https://www.pulumi.com/blog/grounded-ai-why-neo-knows-your-infrastructure/), so the checker grounds every claim in what the change actually does, not in what the writer says it does.\n\n## Where to start\n\nPick the loop you can actually trust first. In order:\n\n1. **Start where “done” is unambiguous.** CI triage, dependency bumps, a flaky-test hunt, a failing job you keep re-running by hand. Loops need an oracle, so begin where the oracle already exists.\n2. **Write the memory file before the loop.** One markdown file, or a board. What is done, what is next, what was tried and failed. This is the spine, and everything else hangs off it.\n3. **Split the checker from the maker.** Use `/goal` with a verifiable condition, or a second agent with its own instructions. Never let the agent that did the work be the one that decides the work is finished.\n4. **Cap it, then read everything.** A max-iteration count, a token budget, a teardown step. Run it once, end to end, then read every line it shipped. The first run is the measurement, not the payoff.\n\nThen look at what you built. You designed it once, and it ran without you steering each step. That is the real shift. But the leverage only holds if you wire the loop like an engineer, not like someone looking for permission to stop thinking. Read what it ships. Keep an opinion. Judgment is the one part that does not move up a layer.\n\nThe loop will do the typing. The thinking is the work.\n\n[See how Pulumi Neo closes the loop on your infrastructure](https://www.pulumi.com/product/neo/)","publishedAt":"2026-06-09T00:00:00.000Z","fetchedAt":"2026-06-09T20:01:37.075Z","url":"https://www.pulumi.com/blog/stop-prompting-design-the-loop/","media":[{"type":"image","url":"https://www.pulumi.com/blog/stop-prompting-design-the-loop/meta.png","r2Key":"releases/cead8414552738210630fec5f940dbc795e02146a45ceefdcb2644f17baa47a9.png","r2Url":"https://media.releases.sh/releases/cead8414552738210630fec5f940dbc795e02146a45ceefdcb2644f17baa47a9.png"}],"coverageCount":0},{"id":"rel_5JvsigOpKqh39c9yDytkE","version":null,"type":"feature","title":"Trigger Deployments on Git Tags","summary":"Pulumi Deployments can now trigger pulumi up automatically when a git tag is pushed, with glob-based tag filters to control which tags deploy. The feature is available across GitHub, GitLab, Bitbucket, Azure DevOps, and custom VCS integrations, and sets the PULUMI_CI_TAG_NAME environment variable for use in pre-run commands or Pulumi programs.","titleGenerated":"Pulumi Deployments now triggers on git tags","titleShort":"Deploy on git tag push","breaking":"unknown","importance":null,"content":"![](https://www.pulumi.com/blog/trigger-deployments-on-git-tags/meta.png)\n\nA git tag is how many teams mark a release as ready. Pulumi Deployments can now act on that signal directly: configure a tag-based trigger, push a version tag like `v1.2.0`, and Pulumi automatically runs `pulumi up` for your stack. No extra pipeline glue, no manual click — your release tag *is* the deployment.\n\n## Why tags?\n\n[Push to Deploy](https://www.pulumi.com/docs/deployments/deployments/using/triggers/#push-to-deploy) has long let you preview changes on a pull request and update a stack when commits merge to a branch. That branch-based model is a great fit for continuous delivery to shared development and QA environments, where every merge should flow straight through.\n\nBut promotion to production is often deliberate, not continuous. You merge throughout the day, then decide — separately — that a particular commit is the release. The conventional way to record that decision is a git tag: `v1.2.0`, `2026.06.0`, `release-2026-06-04`. Tagging is already part of most teams’ release rituals.\n\nTag-based triggers connect that ritual to your infrastructure. Instead of wiring up a separate CI job to call the [Pulumi Deployments REST API](https://www.pulumi.com/docs/deployments/deployments/using/triggers/#rest-api) on a tag event, you configure the trigger once in your stack’s deployment settings and let Pulumi handle the rest.\n\n## How it works\n\nTag triggers are controlled by two settings on your stack’s [deployment configuration](https://www.pulumi.com/docs/deployments/deployments/using/settings/):\n\n- **Run updates for pushed tags** — a toggle that enables running `pulumi up` when a matching tag is pushed.\n- **Tag filters** — a list of glob patterns that decide which tag names qualify.\n\nTag filters use the same model as the [path filters](https://www.pulumi.com/docs/deployments/deployments/using/settings/#path-filtering) you may already know, except the patterns match against the tag name rather than changed file paths. A few examples:\n\n- `v*` — deploy on any tag beginning with `v`, such as `v1.0.0` and `v2.3.1`.\n- `v*` plus `!*-rc*` — deploy on release tags but skip release candidates like `v1.2.0-rc1`.\n- `2026.*` — deploy on calendar-versioned releases such as `2026.06.0`.\n\nFilters prefixed with `!` are exclusions, and an exclusion always wins over an include. With no filters configured and the toggle on, every tag push deploys. Deleting a tag never triggers a deployment.\n\nWhen a tag push kicks off a deployment, Pulumi sets the `PULUMI_CI_TAG_NAME` environment variable to the tag name. Your pre-run commands or your Pulumi program can read it — for example, to stamp the release version onto a resource tag or an application config value.\n\n## Works across every VCS integration\n\nTag triggers are available across all five version control integrations: [GitHub](https://www.pulumi.com/docs/integrations/version-control/github-app/), [GitLab](https://www.pulumi.com/docs/integrations/version-control/gitlab/), [Bitbucket](https://www.pulumi.com/docs/integrations/version-control/bitbucket/), [Azure DevOps](https://www.pulumi.com/docs/integrations/version-control/azure-devops-integration/), and [Custom VCS](https://www.pulumi.com/docs/integrations/version-control/custom-vcs/).\n\n## Get started\n\nYou can configure tag triggers wherever you manage deployment settings today — the [Pulumi Cloud console](https://app.pulumi.com/signin), the [REST API](https://www.pulumi.com/docs/reference/cloud-rest-api/deployments/#patch-settings), or as code with the [`pulumiservice.DeploymentSettings`](https://www.pulumi.com/registry/packages/pulumiservice/api-docs/deploymentsettings/) resource.\n\nTo try it out:\n\n1. Open a stack’s **Settings > Deploy** tab in the Pulumi Cloud console.\n2. Enable **Run updates for pushed tags** and add a tag filter such as `v*`.\n3. Push a tag — `git tag v1.0.0 && git push origin v1.0.0` — and watch the deployment run.\n\nFor the full details, see the [deployment triggers](https://www.pulumi.com/docs/deployments/deployments/using/triggers/#deploying-on-git-tags) and [tag filtering](https://www.pulumi.com/docs/deployments/deployments/using/settings/#tag-filtering) documentation. We’d love to hear how you put tag-based deployments to work.","publishedAt":"2026-06-05T00:00:00.000Z","fetchedAt":"2026-06-05T23:00:34.921Z","url":"https://www.pulumi.com/blog/trigger-deployments-on-git-tags/","media":[{"type":"image","url":"https://www.pulumi.com/blog/trigger-deployments-on-git-tags/meta.png","r2Key":"releases/122f4455f18e4c8c7a202fd6b2eb8edefddab5fff19276e6606e2c11bcd02931.png","r2Url":"https://media.releases.sh/releases/122f4455f18e4c8c7a202fd6b2eb8edefddab5fff19276e6606e2c11bcd02931.png"}],"coverageCount":0},{"id":"rel_7R26Bx1wm7bouuOI2gfPV","version":null,"type":"feature","title":"Use Your Mac for AI Agents: Self-Host Gemma 4 12 B with Pulumi and Tailscale","summary":"New guide shows how to run Google's open-weight Gemma 4 12 B model locally on a Mac using llama.cpp for native inference, k3d for local Kubernetes, Pulumi for infrastructure as code, and Tailscale for secure access. The setup achieves about 20 output tokens per second on an M3 Max MacBook Pro with 36 GB RAM and supports multimodal input including images and audio.","titleGenerated":"Pulumi blog guide: self-host Gemma 4 12 B on Mac with Pulumi and Tailscale","titleShort":"Self-host Gemma 4 on your Mac with Pulumi and Tailscale","breaking":"unknown","importance":null,"content":"![](https://www.pulumi.com/blog/self-host-gemma4-llama-cpp-k8s-tailscale-pulumi/meta.png)\n\nIf you run AI tools and agents, you’ve probably accepted three tradeoffs: your data leaves your network, you can’t work offline, and your bill scales with usage.\n\nOpen-weight models now run well on consumer hardware. Once the model is on your machine, your data stays local, inference works offline, and tokens cost nothing. If you own a modern Mac, you can run a high-quality model yourself.\n\n[Gemma 4](https://blog.google/technology/ai/google-gemma-4/) is an open-weights model family from Google. This post focuses on [Gemma 4 12 B](https://developers.googleblog.com/gemma-4-12b-the-developer-guide/), released in June 2026, using Unsloth’s `Q8_0` [GGUF](https://huggingface.co/docs/hub/en/gguf). The 12 B model fits comfortably on a modern Mac while leaving enough headroom for local `llama.cpp` and a chat UI.\n\nWe’ll use `llama.cpp` for host-native inference, [k3d](https://k3d.io/) for a local Kubernetes cluster, Pulumi for infrastructure as code, and [Tailscale](https://tailscale.com/) for secure access.\n\n## Prerequisites\n\nThis setup was validated on the following hardware:\n\n- macOS 26 Tahoe, version 26.5\n- MacBook Pro with Apple M3 Max\n- 36 GB RAM\n\nOn this machine, `llama.cpp` reported about **20 output tokens per second** for a 160-token validation response with `unsloth/gemma-4-12b-it-GGUF`, `gemma-4-12b-it-Q8_0.gguf`, and a 131,072-token context. Sustained throughput varies by prompt length, thermal state, and `llama.cpp` settings.\n\nYou’ll need `brew`, `docker`, `pulumi`, and `tailscale` installed. We’ll also install `k3d` during the process.\n\n## Run Gemma 4 with host-native llama.cpp\n\nWe use `llama.cpp` directly on macOS to leverage Apple Metal acceleration. Running the LLM on the host is more efficient than trying to pass GPU access into a local Kubernetes VM.\n\nInstall the build tools:\n\n```bash\n<span class=\"line\"><span class=\"cl\">brew install cmake git\n</span></span>\n```\n\nThen build `llama.cpp` from source and download the multimodal projector. In validation, Homebrew `llama.cpp` 9430 could run text inference, but it could not load the new Gemma 4 12 B projector and failed with `unknown projector type: gemma4uv`. Building current `llama.cpp` from source fixed that.\n\n```bash\n<span class=\"line\"><span class=\"cl\"><span class=\"nv\">llm_home</span><span class=\"o\">=</span><span class=\"s2\">\"</span><span class=\"nv\">$HOME</span><span class=\"s2\">/pulumi-gemma4-llm\"</span>\n</span></span><span class=\"line\"><span class=\"cl\">mkdir -p <span class=\"s2\">\"</span><span class=\"nv\">$llm_home</span><span class=\"s2\">/models\"</span> <span class=\"s2\">\"</span><span class=\"nv\">$llm_home</span><span class=\"s2\">/logs\"</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">if</span> <span class=\"o\">[</span> ! -d <span class=\"s2\">\"</span><span class=\"nv\">$llm_home</span><span class=\"s2\">/llama.cpp/.git\"</span> <span class=\"o\">]</span><span class=\"p\">;</span> <span class=\"k\">then</span>\n</span></span><span class=\"line\"><span class=\"cl\"> git clone --depth <span class=\"m\">1</span> https://github.com/ggml-org/llama.cpp.git <span class=\"s2\">\"</span><span class=\"nv\">$llm_home</span><span class=\"s2\">/llama.cpp\"</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">fi</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">cmake -S <span class=\"s2\">\"</span><span class=\"nv\">$llm_home</span><span class=\"s2\">/llama.cpp\"</span> <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> -B <span class=\"s2\">\"</span><span class=\"nv\">$llm_home</span><span class=\"s2\">/llama.cpp/build\"</span> <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> -DGGML_METAL<span class=\"o\">=</span>ON <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> -DGGML_BLAS<span class=\"o\">=</span>ON <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> -DCMAKE_BUILD_TYPE<span class=\"o\">=</span>Release\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">cmake --build <span class=\"s2\">\"</span><span class=\"nv\">$llm_home</span><span class=\"s2\">/llama.cpp/build\"</span> --target llama-server -j <span class=\"m\">10</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">curl -L --fail <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> --output <span class=\"s2\">\"</span><span class=\"nv\">$llm_home</span><span class=\"s2\">/models/mmproj-F16.gguf\"</span> <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> https://huggingface.co/unsloth/gemma-4-12b-it-GGUF/resolve/main/mmproj-F16.gguf\n</span></span>\n```\n\nThen download and run the model with this command:\n\n```bash\n<span class=\"line\"><span class=\"cl\"><span class=\"s2\">\"</span><span class=\"nv\">$HOME</span><span class=\"s2\">/pulumi-gemma4-llm/llama.cpp/build/bin/llama-server\"</span> <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> --hf-repo unsloth/gemma-4-12b-it-GGUF <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> --hf-file gemma-4-12b-it-Q8_0.gguf <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> --mmproj <span class=\"s2\">\"</span><span class=\"nv\">$HOME</span><span class=\"s2\">/pulumi-gemma4-llm/models/mmproj-F16.gguf\"</span> <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> --host 127.0.0.1 <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> --port <span class=\"m\">18080</span> <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> --ctx-size <span class=\"m\">131072</span> <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> --parallel <span class=\"m\">1</span> <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> --jinja <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> --reasoning off\n</span></span>\n```\n\nWe use port `18080` because `8080` is commonly used and is likely to conflict with another service you may already have running locally. If your port `8080` is free, you can use it and adjust the Pulumi config later.\n\nThe model file is about 12.65 GB, and the projector is about 116 MB. Gemma 4 12 B advertises a 131,072-token context, and this Mac loaded that full context with `--parallel 1`. `llama.cpp` projected about 15.1 GiB of Apple Metal device memory for the text model and about 258 MiB worst-case memory for the projector, leaving enough headroom for Open WebUI and the rest of the local stack. The `--reasoning off` flag keeps OpenAI-compatible chat responses visible in clients that do not read separate reasoning fields.\n\nWith `--mmproj`, `/v1/models` advertised `capabilities: [\"completion\",\"multimodal\"]`. In local validation, Open WebUI accepted an uploaded Pulumi logo image and Gemma 4 described it correctly. A small WAV file also worked through the OpenAI-compatible `input_audio` request shape, though `llama.cpp` logs still mark audio input as experimental.\n\n![Open WebUI using local Gemma 4 12 B to describe an uploaded Pulumi logo](https://www.pulumi.com/blog/self-host-gemma4-llama-cpp-k8s-tailscale-pulumi/describe_image.png)\n\n### Verify the LLM API\n\nOpen a new terminal and check if `llama.cpp` is responding:\n\n```bash\n<span class=\"line\"><span class=\"cl\">curl http://127.0.0.1:18080/v1/models\n</span></span>\n```\n\nThe `/v1/models` endpoint should return the model ID `unsloth/gemma-4-12b-it-GGUF`. Now try a chat completion:\n\n```bash\n<span class=\"line\"><span class=\"cl\">curl http://127.0.0.1:18080/v1/chat/completions <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> -H <span class=\"s2\">\"Content-Type: application/json\"</span> <span class=\"se\">\\\n</span></span></span><span class=\"line\"><span class=\"cl\"> -d <span class=\"s1\">'{\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s1\"> \"model\": \"unsloth/gemma-4-12b-it-GGUF\",\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s1\"> \"messages\": [{\"role\": \"user\", \"content\": \"Reply with exactly: OK\"}],\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s1\"> \"temperature\": 0,\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s1\"> \"max_tokens\": 32\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s1\"> }'</span>\n</span></span>\n```\n\nThe chat prompt `Reply with exactly: OK` should return content `OK`. In validation, `llama.cpp` reported an output token velocity of about 20 tokens per second for a longer 160-token response.\n\n### Keep llama.cpp running after reboot\n\nFor a permanent setup, put the `llama.cpp` startup script and logs under a folder in your home directory and let `launchd` restart it when you sign in:\n\n```bash\n<span class=\"line\"><span class=\"cl\"><span class=\"nv\">llm_home</span><span class=\"o\">=</span><span class=\"s2\">\"</span><span class=\"nv\">$HOME</span><span class=\"s2\">/pulumi-gemma4-llm\"</span>\n</span></span><span class=\"line\"><span class=\"cl\">mkdir -p <span class=\"s2\">\"</span><span class=\"nv\">$llm_home</span><span class=\"s2\">/logs\"</span> <span class=\"s2\">\"</span><span class=\"nv\">$HOME</span><span class=\"s2\">/Library/LaunchAgents\"</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">cat > <span class=\"s2\">\"</span><span class=\"nv\">$llm_home</span><span class=\"s2\">/start-llama-server.sh\"</span> <span class=\"s\"><<'EOF'\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\">#!/bin/zsh\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\">set -euo pipefail\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\">export PATH=\"/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin\"\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\">exec \"$HOME/pulumi-gemma4-llm/llama.cpp/build/bin/llama-server\" \\\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> --hf-repo unsloth/gemma-4-12b-it-GGUF \\\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> --hf-file gemma-4-12b-it-Q8_0.gguf \\\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> --mmproj \"$HOME/pulumi-gemma4-llm/models/mmproj-F16.gguf\" \\\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> --host 127.0.0.1 \\\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> --port 18080 \\\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> --ctx-size 131072 \\\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> --parallel 1 \\\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> --jinja \\\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> --reasoning off\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\">EOF</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">chmod +x <span class=\"s2\">\"</span><span class=\"nv\">$llm_home</span><span class=\"s2\">/start-llama-server.sh\"</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">cat > <span class=\"s2\">\"</span><span class=\"nv\">$HOME</span><span class=\"s2\">/Library/LaunchAgents/com.pulumi.gemma4.llama-server.plist\"</span> <span class=\"s\"><<EOF\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"><?xml version=\"1.0\" encoding=\"UTF-8\"?>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"><!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"><plist version=\"1.0\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"><dict>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <key>Label</key>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <string>com.pulumi.gemma4.llama-server</string>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <key>ProgramArguments</key>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <array>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <string>$llm_home/start-llama-server.sh</string>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> </array>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <key>WorkingDirectory</key>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <string>$llm_home</string>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <key>RunAtLoad</key>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <true/>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <key>KeepAlive</key>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <true/>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <key>StandardOutPath</key>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <string>$llm_home/logs/llama-server.out.log</string>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <key>StandardErrorPath</key>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"> <string>$llm_home/logs/llama-server.err.log</string>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"></dict>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\"></plist>\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"s\">EOF</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\">launchctl bootout gui/<span class=\"k\">$(</span>id -u<span class=\"k\">)</span>/com.pulumi.gemma4.llama-server 2>/dev/null <span class=\"o\">||</span> <span class=\"nb\">true</span>\n</span></span><span class=\"line\"><span class=\"cl\">launchctl bootstrap gui/<span class=\"k\">$(</span>id -u<span class=\"k\">)</span> <span class=\"s2\">\"</span><span class=\"nv\">$HOME</span><span class=\"s2\">/Library/LaunchAgents/com.pulumi.gemma4.llama-server.plist\"</span>\n</span></span><span class=\"line\"><span class=\"cl\">launchctl kickstart -k gui/<span class=\"k\">$(</span>id -u<span class=\"k\">)</span>/com.pulumi.gemma4.llama-server\n</span></span>\n```\n\nCheck the `launchd` service and `llama.cpp` logs:\n\n```bash\n<span class=\"line\"><span class=\"cl\">launchctl print gui/<span class=\"k\">$(</span>id -u<span class=\"k\">)</span>/com.pulumi.gemma4.llama-server\n</span></span><span class=\"line\"><span class=\"cl\">tail -f <span class=\"s2\">\"</span><span class=\"nv\">$HOME</span><span class=\"s2\">/pulumi-gemma4-llm/logs/llama-server.err.log\"</span>\n</span></span>\n```\n\nIf you want to stop `llama.cpp` later, unload the `launchd` service:\n\n```bash\n<span class=\"line\"><span class=\"cl\">launchctl bootout gui/<span class=\"k\">$(</span>id -u<span class=\"k\">)</span>/com.pulumi.gemma4.llama-server\n</span></span>\n```\n\n## Deploy Open WebUI with Pulumi and k3d\n\nNow we’ll deploy [Open WebUI](https://github.com/open-webui/open-webui) into a local Kubernetes cluster. This provides a polished chat interface that connects to our host-native LLM.\n\nFirst, install `k3d` if you haven’t already:\n\n```bash\n<span class=\"line\"><span class=\"cl\">brew install k3d\n</span></span>\n```\n\nCreate a new cluster for this project:\n\n```bash\n<span class=\"line\"><span class=\"cl\">k3d cluster create pulumi-gemma4-blog-qa\n</span></span>\n```\n\nWe’ll use the Pulumi program in [`pulumi/examples`](https://github.com/pulumi/examples/tree/master/kubernetes-py-self-host-gemma4-llm). This program defaults to `runtimeMode=host`, which creates a Kubernetes `ExternalName` service pointing to your host machine.\n\nWhy not run the LLM inside Kubernetes on this Mac? Pulumi can do that, and the example supports it with `runtimeMode=cluster`, but that path is meant for Linux hosts with NVIDIA or AMD GPU device plugins.\n\nOn macOS, [`llama.cpp` enables Metal by default](https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md#metal-build), and Metal acceleration is available to native macOS processes. k3d runs Linux containers through Docker Desktop, so those pods do not get direct access to the Mac’s Metal device. Docker’s own [vLLM Metal announcement](https://www.docker.com/blog/docker-model-runner-vllm-metal-macos/) calls out the same boundary: Metal-backed inference runs natively on the host because there is no Metal GPU passthrough for containers. That is why this setup keeps inference host-native and lets Pulumi manage the Kubernetes UI, service wiring, and optional Tailscale access around it.\n\nClone the examples repo, navigate to the program directory, and initialize a new stack:\n\n```bash\n<span class=\"line\"><span class=\"cl\">git clone https://github.com/pulumi/examples.git\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"nb\">cd</span> examples/kubernetes-py-self-host-gemma4-llm\n</span></span><span class=\"line\"><span class=\"cl\">pulumi stack init gemma4-local\n</span></span>\n```\n\nConfigure the program to match your local setup:\n\n```bash\n<span class=\"line\"><span class=\"cl\">pulumi config <span class=\"nb\">set</span> hostLlmPort <span class=\"m\">18080</span>\n</span></span><span class=\"line\"><span class=\"cl\">pulumi config <span class=\"nb\">set</span> llmBaseUrl http://llm-server:18080/v1\n</span></span>\n```\n\nThe Kubernetes service named `llm-server` maps to `host.k3d.internal`. In our validation, we confirmed that a disposable k3d pod could reach the Mac’s `llama.cpp` API at `http://llm-server:18080/v1/models` after a CoreDNS restart.\n\n```bash\n<span class=\"line\"><span class=\"cl\">kubectl rollout restart deployment coredns -n kube-system\n</span></span>\n```\n\nRun `pulumi up` to deploy Open WebUI and connect it to host-native `llama.cpp`:\n\n```bash\n<span class=\"line\"><span class=\"cl\">pulumi up\n</span></span>\n```\n\nIn our validation environment, this command successfully reached the resource synthesis phase without requiring Tailscale credentials because Tailscale exposure is opt-in.\n\n## Access Open WebUI through Tailscale\n\nTailscale allows you to access your private Open WebUI instance from any device on your tailnet. Note that we only expose the web interface, not the raw LLM API, to keep the system secure.\n\nThe base Open WebUI deployment works without Tailscale credentials. To expose the web UI on your tailnet, enable Tailscale resources and provide an explicit `api_key` or OAuth/identity token:\n\n```bash\n<span class=\"line\"><span class=\"cl\">pulumi config <span class=\"nb\">set</span> enableTailscale <span class=\"nb\">true</span>\n</span></span><span class=\"line\"><span class=\"cl\">pulumi config <span class=\"nb\">set</span> tailscale:apiKey YOUR_API_KEY --secret\n</span></span>\n```\n\nOnce configured, Pulumi will create a Tailscale device or proxy that routes traffic to your Open WebUI service.\n\n## Use the model with Pi\n\nOpen WebUI gives you a browser-based chat interface, but local models are also useful from coding agents. Pi is the local coding agent used for this validation; if you do not use Pi, treat this section as an example of how any OpenAI-compatible client can point at the same local endpoint. Pi can point at the same OpenAI-compatible `llama.cpp` endpoint and use the model running on your Mac.\n\nFor a fresh Pi config, create `~/.pi/agent/models.json` with a local provider that points at the `llama.cpp` API:\n\n```json\n<span class=\"line\"><span class=\"cl\"><span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"providers\"</span><span class=\"p\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"local-llama\"</span><span class=\"p\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"baseUrl\"</span><span class=\"p\">:</span> <span class=\"s2\">\"http://127.0.0.1:18080/v1\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"api\"</span><span class=\"p\">:</span> <span class=\"s2\">\"openai-completions\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"apiKey\"</span><span class=\"p\">:</span> <span class=\"s2\">\"local\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"compat\"</span><span class=\"p\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"supportsDeveloperRole\"</span><span class=\"p\">:</span> <span class=\"kc\">false</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"supportsReasoningEffort\"</span><span class=\"p\">:</span> <span class=\"kc\">false</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">},</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"models\"</span><span class=\"p\">:</span> <span class=\"p\">[</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"id\"</span><span class=\"p\">:</span> <span class=\"s2\">\"unsloth/gemma-4-12b-it-GGUF\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"name\"</span><span class=\"p\">:</span> <span class=\"s2\">\"Gemma 4 12B Q8 (local llama.cpp)\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"reasoning\"</span><span class=\"p\">:</span> <span class=\"kc\">false</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"input\"</span><span class=\"p\">:</span> <span class=\"p\">[</span><span class=\"s2\">\"text\"</span><span class=\"p\">],</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"contextWindow\"</span><span class=\"p\">:</span> <span class=\"mi\">131072</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"maxTokens\"</span><span class=\"p\">:</span> <span class=\"mi\">1024</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"cost\"</span><span class=\"p\">:</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"input\"</span><span class=\"p\">:</span> <span class=\"mi\">0</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"output\"</span><span class=\"p\">:</span> <span class=\"mi\">0</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"cacheRead\"</span><span class=\"p\">:</span> <span class=\"mi\">0</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"cacheWrite\"</span><span class=\"p\">:</span> <span class=\"mi\">0</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">]</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span>\n```\n\nThen set Pi to use that provider and model by default in `~/.pi/agent/settings.json`:\n\n```json\n<span class=\"line\"><span class=\"cl\"><span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"defaultProvider\"</span><span class=\"p\">:</span> <span class=\"s2\">\"local-llama\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"defaultModel\"</span><span class=\"p\">:</span> <span class=\"s2\">\"unsloth/gemma-4-12b-it-GGUF\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"defaultThinkingLevel\"</span><span class=\"p\">:</span> <span class=\"s2\">\"off\"</span><span class=\"p\">,</span>\n</span></span><span class=\"line\"><span class=\"cl\"> <span class=\"nt\">\"hideThinkingBlock\"</span><span class=\"p\">:</span> <span class=\"kc\">true</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span>\n```\n\nIf you already have Pi configuration files, merge the `local-llama` provider and defaults into your existing JSON instead of replacing the files.\n\n![Pi connected to local Gemma 4 through llama.cpp](https://www.pulumi.com/blog/self-host-gemma4-llama-cpp-k8s-tailscale-pulumi/pi.png)\n\n## Advanced: Linux GPU in-cluster serving\n\nIf you’re running on a Linux host with an NVIDIA or AMD GPU, you can run the LLM directly inside the Kubernetes cluster. This requires the NVIDIA or ROCm device plugins.\n\nThe Pulumi program supports this through `runtimeMode=cluster`. In this mode, it deploys a `LlmServer` pod that manages the `llama.cpp` process within the cluster, using GPU resource requests to ensure hardware acceleration.\n\n## Cleanup\n\nWhen you’re done, you can tear down the resources:\n\n```bash\n<span class=\"line\"><span class=\"cl\">pulumi destroy\n</span></span><span class=\"line\"><span class=\"cl\">k3d cluster delete pulumi-gemma4-blog-qa\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\"># Stop llama.cpp using the PID from your terminal</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"nb\">kill</span> <PID>\n</span></span>\n```","publishedAt":"2026-06-04T00:00:00.000Z","fetchedAt":"2026-06-04T22:02:52.131Z","url":"https://www.pulumi.com/blog/self-host-gemma4-llama-cpp-k8s-tailscale-pulumi/","media":[{"type":"image","url":"https://www.pulumi.com/blog/self-host-gemma4-llama-cpp-k8s-tailscale-pulumi/meta.png","r2Key":"releases/4dd5187c408520eccb5cc7d79dca3e96ac2c7c1020b04e669db9b006423f8829.png","r2Url":"https://media.releases.sh/releases/4dd5187c408520eccb5cc7d79dca3e96ac2c7c1020b04e669db9b006423f8829.png"},{"type":"image","url":"https://www.pulumi.com/blog/self-host-gemma4-llama-cpp-k8s-tailscale-pulumi/describe_image.png","alt":"Open WebUI using local Gemma 4 12 B to describe an uploaded Pulumi logo","r2Key":"releases/3b3788bec1ffd4b18202c43c9e34b3025e89ca44203c06f1a4aae58d28d8a764.png","r2Url":"https://media.releases.sh/releases/3b3788bec1ffd4b18202c43c9e34b3025e89ca44203c06f1a4aae58d28d8a764.png"},{"type":"image","url":"https://www.pulumi.com/blog/self-host-gemma4-llama-cpp-k8s-tailscale-pulumi/pi.png","alt":"Pi connected to local Gemma 4 through llama.cpp","r2Key":"releases/0a347511acbfaef16e5415b0c852519f90cad5d433a8ffdbace3c51f2a9b49c9.png","r2Url":"https://media.releases.sh/releases/0a347511acbfaef16e5415b0c852519f90cad5d433a8ffdbace3c51f2a9b49c9.png"}],"coverageCount":0}],"pagination":{"nextCursor":"2026-06-04T00:00:00.000Z|2026-06-04T22:02:52.131Z|rel_7R26Bx1wm7bouuOI2gfPV","limit":20},"summaries":{"rolling":{"windowDays":90,"summary":"Pulumi shipped broad platform expansion across AI infrastructure management, governance tooling, and developer experience. Bun graduated from package manager to first-class runtime for Pulumi programs, eliminating the Node.js dependency for TypeScript execution. Pulumi Neo gained critical collaboration features—Plan Mode for deliberate upfront discovery before execution, read-only task permissions to sandbox AI operations, and task sharing for organizational visibility—while Neo's migration capabilities expanded to orchestrate zero-downtime transitions from CDK, Terraform, and ARM templates. On the governance side, Pulumi shipped `pulumi policy analyze` to validate stack state against policies without running programs, locked down ESC values with `fn::final` to prevent child environment overrides, and introduced schema validation and Terraform state backend support to unify infrastructure management across tools. A 20x performance improvement from journaling moved to general availability, and the platform extended identity options with Google Sign-In, multi-VCS support (GitHub Enterprise, Azure DevOps, GitLab), and Pulumi IAM availability for self-hosted deployments.","releaseCount":44,"generatedAt":"2026-04-11T14:00:45.384Z"},"monthly":[{"year":2026,"month":3,"summary":"Pulumi focused on safety and control throughout March, launching read-only and plan-first modes for Neo alongside expanded IAM capabilities for tag-based access and team role assignments. The month also brought governance advances—OPA/Rego reached stable parity with native policy SDKs, ESC gained fn::final to lock down inherited values, and Insights expanded to customer-managed runners for regulated environments. Infrastructure tooling broadened with Terraform state backend support in Pulumi Cloud, Google Sign-In for authentication, and multi-provider version control integration, while journaling—a 20x performance improvement shipped in January—graduated to general availability by default.","releaseCount":14,"generatedAt":"2026-04-11T14:00:48.148Z"}]}}