<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0"><channel><title>Pulumi Blog: Engineering</title><link>https://www.pulumi.com/blog/tag/engineering/</link><description>Pulumi blog posts: Engineering.</description><language>en-us</language><pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate><item><title>How We Built a Distributed Work Scheduling System for Pulumi Cloud</title><link>https://www.pulumi.com/blog/how-we-built-a-distributed-work-scheduling-system-for-pulumi-cloud/</link><pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate><guid>https://www.pulumi.com/blog/how-we-built-a-distributed-work-scheduling-system-for-pulumi-cloud/</guid><description>
&lt;img src="https://www.pulumi.com/images/generated/blog/how-we-built-a-distributed-work-scheduling-system-for-pulumi-cloud/index.png" /&gt;
&lt;p&gt;Pulumi Cloud orchestrates a growing number of workflow types: &lt;a href="https://www.pulumi.com/docs/deployments/"&gt;Deployments&lt;/a&gt;, &lt;a href="https://www.pulumi.com/docs/insights/"&gt;Insights&lt;/a&gt; discovery scans, and &lt;a href="https://www.pulumi.com/docs/insights/policy/"&gt;policy evaluations&lt;/a&gt;. Some of that work runs on Pulumi&amp;rsquo;s infrastructure, and some of it runs on yours via &lt;a href="https://www.pulumi.com/docs/deployments/deployments/customer-managed-agents/"&gt;customer-managed workflow runners&lt;/a&gt;. We needed a scheduling system that could handle all of these workflow types reliably across both environments. In this post, we&amp;rsquo;ll take a look at the system we built.&lt;/p&gt;
&lt;h2 id="where-we-started"&gt;Where we started&lt;/h2&gt;
&lt;p&gt;For our first workflow integration, Deployments, scheduling wasn&amp;rsquo;t too complicated. A deployment was queued, a worker picked it up, and it ran. The queue was purpose-built for deployments, and it worked well for that single use case. Over time, we added more sophisticated logic to handle retries, ordering, rate limiting, observability, and more.&lt;/p&gt;
&lt;p&gt;With the launch of Insights, the number of workflow types grew. Now Pulumi Cloud manages discovery scans to catalog cloud resources and runs audit policy evaluations to continuously verify compliance. While these workflows share similarities, each type needed its own scheduling, retry logic, and failure handling.&lt;/p&gt;
&lt;p&gt;Later we added the option for customers to run workflows on their own infrastructure using &lt;a href="https://www.pulumi.com/docs/deployments/deployments/customer-managed-agents/"&gt;customer-managed workflow runners&lt;/a&gt;. As the complexity of these requirements grew, we knew that our initial approach for Deployments wasn&amp;rsquo;t going to scale. We needed a single system that could schedule any type of work, route it to the right place, and handle the messy reality of distributed execution: crashes, network failures, rate limits, and retries.&lt;/p&gt;
&lt;p&gt;We call this the &lt;strong&gt;background activity system&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="why-not-use-an-off-the-shelf-queue"&gt;Why not use an off-the-shelf queue?&lt;/h2&gt;
&lt;p&gt;Why build this instead of using Amazon SQS, RabbitMQ, or one of the many existing queue libraries? We considered these options but chose to build our own for a few reasons.&lt;/p&gt;
&lt;p&gt;Pulumi Cloud supports &lt;a href="https://www.pulumi.com/docs/administration/self-hosting/"&gt;self-hosted installations&lt;/a&gt;, including air-gapped environments. We intentionally minimize external dependencies so that self-hosted customers don&amp;rsquo;t have to stand up additional infrastructure. A system built on an external queue works fine for our hosted service, but it means self-hosted customers would need to provide a compatible backend. By building on top of the database we already require, we avoid adding another system to maintain.&lt;/p&gt;
&lt;p&gt;More importantly, queueing is only part of the problem. What we actually need is &lt;em&gt;scheduling with durability&lt;/em&gt;. This means ensuring that remote workers don&amp;rsquo;t lose activities on restart, priority so that urgent work gets compute resources first, constraints like &amp;ldquo;only so many scans per org at a time,&amp;rdquo; structured logging for observability, and checkpointing so that long-running operations can resume after a failure.&lt;/p&gt;
&lt;p&gt;These features can be layered onto a generic queue library but can require more code than implementing them directly. For example, priority queues are often implemented with multiple ranked queues, but this breaks single-activity-at-a-time constraints. A second queue wouldn&amp;rsquo;t see a job already running in the first one. There&amp;rsquo;s no way for producers in a distributed system to coordinate across the queues without support in the queuing system itself.&lt;/p&gt;
&lt;p&gt;Capacity management is another area where generic queues fall short. Distributed systems need to respond dynamically to slowdowns, network interruptions, and rate limits from downstream services. These are common low-level details that every workflow type needs, and building them into the scheduling layer means individual handlers don&amp;rsquo;t have to solve them independently.&lt;/p&gt;
&lt;p&gt;We also need structured logging that works everywhere, including on customer-managed runners behind firewalls where centralized logging services aren&amp;rsquo;t accessible.&lt;/p&gt;
&lt;p&gt;Building this ourselves gave us a system that works with existing infrastructure and handles these requirements natively.&lt;/p&gt;
&lt;h2 id="design-constraints"&gt;Design constraints&lt;/h2&gt;
&lt;p&gt;With that context, here are some of the constraints that shaped the design:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pull-only agents.&lt;/strong&gt; Customer-managed workflow runners live behind NATs, corporate proxies, and air-gapped networks. They can&amp;rsquo;t accept inbound connections, so all communication has to be agent-initiated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mixed execution environments.&lt;/strong&gt; The same system needs to work for Pulumi-hosted workers (with direct access to internal systems) and customer-managed runners (communicating entirely over REST). We didn&amp;rsquo;t want to maintain two separate code paths.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Different workflow types.&lt;/strong&gt; Deployments, Insights scans, and audit policy evaluations have different payloads and execution semantics, but they all need the same scheduling guarantees: exactly-once execution, automatic retries, failure recovery, and observability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic fault tolerance.&lt;/strong&gt; Agents crash, networks drop, and machines get recycled by autoscalers. The system needs to detect these failures and recover without needing a person to step in.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extensibility.&lt;/strong&gt; We knew we&amp;rsquo;d keep adding workflow types. Adding a new one should mean writing a handler and registering it, not building new infrastructure.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-background-activity"&gt;The background activity&lt;/h2&gt;
&lt;p&gt;At the center of the system is the &lt;strong&gt;background activity&lt;/strong&gt;, a persistent, typed work unit. Each activity includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;type discriminator&lt;/strong&gt; that identifies what kind of work it represents (e.g., &amp;ldquo;insights-discovery&amp;rdquo; or &amp;ldquo;policy-evaluation&amp;rdquo;)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;payload&lt;/strong&gt; specific to that type, containing whatever data the handler needs&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;routing context&lt;/strong&gt; that determines which runner pool should execute it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scheduling metadata&lt;/strong&gt; like priority, activation time, and retry configuration&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;status&lt;/strong&gt; tracking where the activity is in its lifecycle&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The type discriminator makes this system polymorphic. The scheduling engine doesn&amp;rsquo;t need to know what&amp;rsquo;s inside the payload. It moves activities through their lifecycle and delegates the actual work to a type-specific handler.&lt;/p&gt;
&lt;h3 id="the-state-machine"&gt;The state machine&lt;/h3&gt;
&lt;p&gt;Every activity follows the same lifecycle regardless of type:&lt;/p&gt;
&lt;pre class="mermaid"&gt;
---
config:
flowchart:
curve: linear
---
graph LR
Start(( )) --&amp;gt;|Created| Ready
Ready --&amp;gt;|Leased| Pending
Pending --&amp;gt;|Started| Executing
Executing --&amp;gt;|Success| Completed
Completed --&amp;gt; End(( ))
Executing --&amp;gt;|Error| Failed
Failed --&amp;gt; End
Executing --&amp;gt;|Canceled| Canceled
Canceled --&amp;gt; End
Executing --&amp;gt;|Dependencies| Waiting
Waiting --&amp;gt;|Unblocked| Ready
Pending --&amp;gt;|Lease expired| Restarting
Executing --&amp;gt;|Lease expired| Restarting
Restarting --&amp;gt;|Re-lease| Ready
style Start fill:#000,stroke:#000,color:#000
style End fill:#000,stroke:#000,color:#000
&lt;/pre&gt;
&lt;p&gt;The states fall into two groups:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Running states&lt;/strong&gt; (work is in flight or can be resumed):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ready&lt;/strong&gt;: queued and eligible to be claimed by a worker&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pending&lt;/strong&gt;: claimed by a worker, execution about to start&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Executing&lt;/strong&gt;: actively running on a worker&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Waiting&lt;/strong&gt;: parked, blocked on one or more dependency activities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Restarting&lt;/strong&gt;: recovered after a worker failure, ready to be re-claimed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Terminal states&lt;/strong&gt; (work is done):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Completed&lt;/strong&gt;, &lt;strong&gt;Failed&lt;/strong&gt;, &lt;strong&gt;Canceled&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;New workflow types get these features automatically: scheduling, retries, dependency management, and observability.&lt;/p&gt;
&lt;h2 id="leases-distributed-execution-without-coordination"&gt;Leases: distributed execution without coordination&lt;/h2&gt;
&lt;p&gt;A central challenge of any distributed work queue is preventing double-execution. If two agents try to execute the same activity simultaneously, you get duplicate work and data corruption. A central coordinator can solve this, but it becomes a single point of failure.&lt;/p&gt;
&lt;p&gt;We use lease-based optimistic concurrency instead. This is a well-known pattern, adapted here for long-running, stateful workflows.&lt;/p&gt;
&lt;h3 id="how-it-works"&gt;How it works&lt;/h3&gt;
&lt;p&gt;When an agent is ready for new work, it asks the service to &lt;strong&gt;lease&lt;/strong&gt; an activity. The service atomically selects the highest-priority ready activity, assigns a lease token with an expiration time, and transitions the activity to &lt;code&gt;Pending&lt;/code&gt;. No other agent can claim the same activity.&lt;/p&gt;
&lt;pre class="mermaid"&gt;
sequenceDiagram
participant Agent
participant Service
Agent-&amp;gt;&amp;gt;Service: Poll for work
Note right of Service: Select highest-priority&amp;lt;br/&amp;gt;Ready activity
Note right of Service: Atomically set lease&amp;lt;br/&amp;gt;token + expiration
Service-&amp;gt;&amp;gt;Agent: Lease (token, expiration)
Agent-&amp;gt;&amp;gt;Service: Begin execution
Note right of Service: Transition to Executing
Note over Agent: Work in progress...
Agent-&amp;gt;&amp;gt;Service: Renew lease
Service-&amp;gt;&amp;gt;Agent: New expiration
Note over Agent: Work continues...
Agent-&amp;gt;&amp;gt;Service: Complete (token, result)
Note right of Service: Transition to Completed&amp;lt;br/&amp;gt;Archive activity
Service-&amp;gt;&amp;gt;Agent: Acknowledged
&lt;/pre&gt;
&lt;p&gt;While executing, the agent periodically &lt;strong&gt;renews&lt;/strong&gt; its lease to signal that it&amp;rsquo;s still working. If the agent crashes, loses network connectivity, or is terminated, it stops renewing. Once the lease expires, the service transitions the activity to &lt;code&gt;Restarting&lt;/code&gt;, making it available for another agent to claim.&lt;/p&gt;
&lt;pre class="mermaid"&gt;
sequenceDiagram
participant A as Agent A
participant S as Service
participant B as Agent B
A-&amp;gt;&amp;gt;S: Lease activity
S-&amp;gt;&amp;gt;A: Token + expiration
Note over A: Executing...
A--xS: Agent A crashes
Note right of S: Lease expires
Note right of S: Transition → Restarting
B-&amp;gt;&amp;gt;S: Poll for work
Note right of S: Lease to Agent B&amp;lt;br/&amp;gt;Transition → Pending
S-&amp;gt;&amp;gt;B: Token + expiration
Note over B: Agent B continues execution
&lt;/pre&gt;
&lt;p&gt;The service doesn&amp;rsquo;t need to explicitly coordinate between workers because leases are acquired using atomic database operations. The lease expiration is the failure detector; if a lease expires, then the work needs to be rescheduled.&lt;/p&gt;
&lt;h2 id="routing-work-to-the-right-runner-pool"&gt;Routing work to the right runner pool&lt;/h2&gt;
&lt;p&gt;Pulumi Cloud supports multiple &lt;a href="https://www.pulumi.com/docs/deployments/deployments/customer-managed-agents/"&gt;workflow runner pools&lt;/a&gt;. An organization might have one pool for production in &lt;code&gt;us-east-1&lt;/code&gt;, another for staging in &lt;code&gt;eu-west-1&lt;/code&gt;, and use Pulumi-hosted runners for development. Work needs to reach the right pool.&lt;/p&gt;
&lt;p&gt;Each activity carries a &lt;strong&gt;routing context&lt;/strong&gt; that identifies which runner pool should execute it. When a runner polls for work, it filters by its own pool identifier so that it only sees activities meant for it.&lt;/p&gt;
&lt;p&gt;We use prefix matching for this filtering. A runner matches activities whose context starts with its pool&amp;rsquo;s identifier. This means the service can use hierarchical contexts (e.g., &lt;code&gt;pool-abc/insights/scan-123&lt;/code&gt;) and runners will still match on the pool prefix. Cleanup is also straightforward; when a runner pool is deleted, all activities with that context prefix are bulk-canceled.&lt;/p&gt;
&lt;p&gt;This routing mechanism works the same way regardless of workflow type, and adding a new workflow type doesn&amp;rsquo;t require changes to the routing layer.&lt;/p&gt;
&lt;h2 id="dependencies-and-multi-step-workflows"&gt;Dependencies and multi-step workflows&lt;/h2&gt;
&lt;p&gt;Some workflows are naturally multi-step. An Insights discovery scan might discover resources that then need policy evaluation. Rather than building a separate orchestration engine, we built dependency management into the activity system.&lt;/p&gt;
&lt;p&gt;An activity can declare a &lt;strong&gt;dependency set&lt;/strong&gt;: a list of other activities that must complete before it can run. A dependent activity enters the &lt;code&gt;Waiting&lt;/code&gt; state when created. As its dependencies complete, the system checks whether all prerequisites are satisfied. When the last one finishes, the waiting activity transitions to &lt;code&gt;Ready&lt;/code&gt; and enters the scheduling queue.&lt;/p&gt;
&lt;pre class="mermaid"&gt;
graph TD
A[&amp;#34;Insights Discovery&amp;lt;br/&amp;gt;&amp;lt;b&amp;gt;Executing&amp;lt;/b&amp;gt;&amp;#34;] --&amp;gt;|depends on| B[&amp;#34;Policy Evaluation&amp;lt;br/&amp;gt;&amp;lt;b&amp;gt;Waiting&amp;lt;/b&amp;gt;&amp;#34;]
A --&amp;gt;|completes| C[&amp;#34;Insights Discovery&amp;lt;br/&amp;gt;&amp;lt;b&amp;gt;Completed&amp;lt;/b&amp;gt;&amp;#34;]
C --&amp;gt;|triggers| D[&amp;#34;Policy Evaluation&amp;lt;br/&amp;gt;&amp;lt;b&amp;gt;Ready&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;(auto-scheduled)&amp;#34;]
&lt;/pre&gt;
&lt;p&gt;This gives us a lightweight &lt;a href="https://en.wikipedia.org/wiki/Directed_acyclic_graph"&gt;DAG&lt;/a&gt; of work without requiring a separate workflow engine. Dependent activities get the same guarantees as any other activity: lease-based execution, automatic recovery, and observability.&lt;/p&gt;
&lt;h2 id="two-execution-modes-one-interface"&gt;Two execution modes, one interface&lt;/h2&gt;
&lt;p&gt;This is where the design really pays off for customer-managed runners. The system supports two execution modes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Direct mode&lt;/strong&gt; runs in-process alongside the Pulumi Cloud service. Workers have low-latency access to internal systems and can process activities with minimal overhead. This is what Pulumi-hosted runners use.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remote mode&lt;/strong&gt; communicates over REST APIs. The runner polls for activities, leases them, executes work locally, and reports results back over HTTP. This is what customer-managed runners use. No database access, no internal network access, no inbound connectivity required.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both modes share the same handler interface so that a workflow handler doesn&amp;rsquo;t need to know where it&amp;rsquo;s running. Whether it&amp;rsquo;s running on Pulumi&amp;rsquo;s hosted infrastructure or on a customer&amp;rsquo;s Kubernetes cluster, the handler simply processes the payload and reports a result.&lt;/p&gt;
&lt;h2 id="putting-it-all-together"&gt;Putting it all together&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s walk through a concrete example. A user wants to run an Insights discovery scan on an AWS account using a customer-managed workflow runner.&lt;/p&gt;
&lt;pre class="mermaid"&gt;
sequenceDiagram
participant User
participant PC as Pulumi Cloud
participant Runner as Workflow Runner&amp;lt;br/&amp;gt;(Customer Infrastructure)
User-&amp;gt;&amp;gt;PC: 1. Configure Insights scan&amp;lt;br/&amp;gt;for runner pool
Note right of PC: 2. Create background activity&amp;lt;br/&amp;gt;type: insights-discovery&amp;lt;br/&amp;gt;context: runner-pool-xyz&amp;lt;br/&amp;gt;status: Ready
Runner-&amp;gt;&amp;gt;PC: 3. Poll for work (filtered by pool)
PC-&amp;gt;&amp;gt;Runner: 4. Lease activity (token + expiration)
Runner-&amp;gt;&amp;gt;PC: 5. Initialize workflow
PC-&amp;gt;&amp;gt;Runner: Return cloud credentials + job token
Note over Runner: 6. Execute scan locally&amp;lt;br/&amp;gt;(talks directly to cloud APIs —&amp;lt;br/&amp;gt;credentials are used only on&amp;lt;br/&amp;gt;the runner)
Runner-&amp;gt;&amp;gt;PC: 7. Renew lease
Note right of PC: Extend expiration
Runner-&amp;gt;&amp;gt;PC: 8. Report completion
Note right of PC: 9. Mark completed&amp;lt;br/&amp;gt;Archive activity
Note right of PC: 10. Unblock dependent activities&amp;lt;br/&amp;gt;(e.g., policy evaluation)
&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;A user configures an AWS account for Insights scanning in Pulumi Cloud and assigns it to a workflow runner pool.&lt;/li&gt;
&lt;li&gt;Pulumi Cloud creates a background activity with the type set to insights discovery, the routing context set to the runner pool, and the payload containing the account configuration.&lt;/li&gt;
&lt;li&gt;A customer-managed workflow runner polling that pool detects new work.&lt;/li&gt;
&lt;li&gt;The runner leases the activity, acquiring an exclusive lock via the lease token.&lt;/li&gt;
&lt;li&gt;The runner initializes the workflow, receiving any required cloud provider credentials (e.g., resolved from &lt;a href="https://www.pulumi.com/docs/esc/"&gt;Pulumi ESC (Environments, Secrets, and Configuration)&lt;/a&gt;) and a job token from Pulumi Cloud.&lt;/li&gt;
&lt;li&gt;The runner executes the scan locally on the customer&amp;rsquo;s infrastructure, talking directly to the cloud provider APIs.&lt;/li&gt;
&lt;li&gt;During execution, the runner periodically renews its lease to signal liveness.&lt;/li&gt;
&lt;li&gt;The scan completes, and the runner reports the result back to Pulumi Cloud.&lt;/li&gt;
&lt;li&gt;The service marks the activity as completed and archives it.&lt;/li&gt;
&lt;li&gt;If a dependent policy evaluation activity was waiting on this scan, it automatically transitions to &lt;code&gt;Ready&lt;/code&gt; and enters the scheduling queue, where another runner in the pool can pick it up.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This flow works the same way whether the runner is hosted by Pulumi or by the customer. The only difference is whether the execution mode is direct or remote.&lt;/p&gt;
&lt;h2 id="retries-and-scheduling"&gt;Retries and scheduling&lt;/h2&gt;
&lt;p&gt;Failures are expected in distributed systems. The background activity system handles them at several levels:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lease expiration&lt;/strong&gt; covers hard failures like agent crashes, network partitions, and machine terminations. If a lease expires, the activity moves to &lt;code&gt;Restarting&lt;/code&gt;, and is available for another agent to pick up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handler-controlled retries&lt;/strong&gt; cover soft failures like transient API errors and rate limits. A handler can request a reschedule with a delay, putting the activity back in &lt;code&gt;Ready&lt;/code&gt; with a future activation time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic retries&lt;/strong&gt; provide a configurable retry budget per activity. Each activity can specify how many times it should be retried and the delay between attempts, preventing runaway retry loops.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Priority scheduling&lt;/strong&gt; ensures urgent work gets processed first. Higher-priority activities are leased before lower-priority ones, even if the lower-priority activity has been waiting longer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lease renewal during slowdowns&lt;/strong&gt; keeps the activity alive without blocking other work, even if a downstream service is slow. The agent continues renewing its lease while it waits, and the scheduler remains free to assign other activities to other agents.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="observability"&gt;Observability&lt;/h2&gt;
&lt;p&gt;Every activity generates a structured log of its execution, including timestamps, severity levels, and code context. Logs are stored with the activity record and are accessible via an API and admin tooling.&lt;/p&gt;
&lt;p&gt;This is especially useful for customer-managed runners, where the service can&amp;rsquo;t directly observe the execution environment. The structured log gives operators visibility into the execution context, even when the runner is behind a firewall. Handlers can also use these logs as a progress journal, encoding checkpoints that allow a restarted activity to pick up where it left off rather than starting from scratch.&lt;/p&gt;
&lt;p&gt;Retention policies are configurable per organization and per workflow type. Completed activities can be retained for auditing or purged to manage storage, and failed activities are typically retained longer for debugging.&lt;/p&gt;
&lt;h2 id="what-we-learned"&gt;What we learned&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;A generic system pays off quickly.&lt;/strong&gt; Our initial instinct was to build targeted solutions for each workflow type. Investing in a generic activity system required more upfront design work, but now adding a new workflow type requires a fraction of the effort it would take otherwise. New workflows ship with full scheduling, retry, and observability support from day one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Leases handle many failure modes.&lt;/strong&gt; We evaluated several approaches for distributed work coordination, including message queues with explicit acknowledgment and coordinator-based assignment. The lease model works well because all failure modes are handled through timeouts. If an agent is running as expected, it renews. If it isn&amp;rsquo;t, the lease expires.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keeping the execution paths symmetric requires discipline.&lt;/strong&gt; Making the hosted and self-hosted paths share the same handler interface was a deliberate choice. It would be easy to add shortcuts for the hosted path that bypass the remote API, but resisting that temptation means that features work for both cases automatically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The hard part isn&amp;rsquo;t running the work.&lt;/strong&gt; Running a scan or a deployment is straightforward once you have the right credentials. The real complexity is in everything around the execution: scheduling, routing, leasing, retrying, resolving dependencies, and cleaning up. These operational concerns aren&amp;rsquo;t visible to users, but they are essential to providing a reliable experience.&lt;/p&gt;
&lt;h2 id="wrapping-it-up"&gt;Wrapping it up&lt;/h2&gt;
&lt;p&gt;Today this system powers deployments, Insights discovery scans, and policy evaluations across both Pulumi Cloud and customer-managed infrastructure. The architecture is general enough that every new workflow type we add inherits the full scheduling, routing, retry, and observability stack without additional plumbing.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re interested in running workflows on your own infrastructure, check out &lt;a href="https://www.pulumi.com/docs/deployments/deployments/customer-managed-agents/"&gt;customer-managed workflow runners&lt;/a&gt;. To see how Insights can help you understand and manage your cloud infrastructure, &lt;a href="https://www.pulumi.com/docs/insights/"&gt;get started with Pulumi Insights&lt;/a&gt;.&lt;/p&gt;</description><author>Levi Blackstone</author><author>Davide Massarenti</author><category>pulumi-cloud</category><category>features</category><category>engineering</category><category>insights</category></item><item><title>Benchmarking Python Performance</title><link>https://www.pulumi.com/blog/benchmarking-python-performance/</link><pubDate>Thu, 28 Sep 2023 00:00:00 +0000</pubDate><guid>https://www.pulumi.com/blog/benchmarking-python-performance/</guid><description>
&lt;img src="https://www.pulumi.com/images/generated/blog/benchmarking-python-performance/index.png" /&gt;
&lt;p&gt;This is the second post in a series about performance optimizations we&amp;rsquo;ve made
to the Pulumi CLI and SDKs. In this post, we&amp;rsquo;ll go deep on a performance
improvement we made for Pulumi Python programs. You can read more
about Amazing Performance in
&lt;a href="https://www.pulumi.com/blog/amazing-performance/"&gt;the first post in the series&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Late last year, we took a hard look at the performance of Python programs when we
realized they weren&amp;rsquo;t performing up to our expectations. We uncovered a major
bug limiting Python performance, and we ran a number of rigorous experiments
to evaluate just how performant Pulumi Python programs are after the bug had
been repaired. The results indicate Pulumi Python programs are significantly
faster than they were, and now Pulumi Python has reached performance parity
with Pulumi Node.js!&lt;/p&gt;
&lt;h2 id="the-bug"&gt;The Bug&lt;/h2&gt;
&lt;p&gt;When you execute a Pulumi program, Pulumi internally builds a dependency graph
between the resources in your program. In every Pulumi program, some resources
have all their input arguments available at the time of their construction.
In contrast, other resources may depend on &lt;code&gt;Outputs&lt;/code&gt; from other resources.&lt;/p&gt;
&lt;p&gt;For example, consider a sample program where we create two AWS S3 buckets, where
one bucket is used to store logs for the other bucket:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pulumi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pulumi_aws&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;aws&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;log_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;logBucket&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;log-delivery-write&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bucket&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;acl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;private&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;loggings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BucketLoggingArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;target_bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;target_prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;log/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Because &lt;code&gt;bucket&lt;/code&gt; takes an &lt;code&gt;Output&lt;/code&gt; from &lt;code&gt;log_bucket&lt;/code&gt; as an input,
we can&amp;rsquo;t create the &lt;code&gt;bucket&lt;/code&gt; until after the &lt;code&gt;log_bucket&lt;/code&gt;
is created. We have to create the &lt;code&gt;log_bucket&lt;/code&gt; first to compute its ID,
which we can pass to &lt;code&gt;bucket&lt;/code&gt;. This idea extends inductively for
arbitrary programs – before any resource can be run, we must resolve the
&lt;code&gt;Outputs&lt;/code&gt; of all of its arguments. To do this, Pulumi builds a dependency graph
between all resources in your program. Then, it walks the graph topologically
to schedule provisioning operations.&lt;/p&gt;
&lt;p&gt;Provisioning operations that are not dependent on each other can be executed
in parallel, and Pulumi defaults to unbounded parallelism, but users can
ratchet this down if they so desire. Consider this embarrassingly parallel
Python program:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pulumi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pulumi_aws&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;aws&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# SQS&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;pulumi-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rjust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# SNS&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;pulumi-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rjust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this program, we can create 200 resources in parallel because none of them
take inputs from other resources. This program should be entirely
network-bound because Pulumi can issue all 200 API calls in parallel and wait
for AWS to provision the instances.
&lt;a href="https://github.com/pulumi/pulumi/issues/11116"&gt;We discovered&lt;/a&gt;, however,
that it did not! Strangely, API calls were issued in an initial batch of 20;
as one completed, another would start.&lt;/p&gt;
&lt;h2 id="the-fix"&gt;The Fix&lt;/h2&gt;
&lt;p&gt;The culprit was the Python default future executor,
&lt;a href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor"&gt;ThreadPoolExecutor&lt;/a&gt;.
We observed that benchmark was run on a four-core computer, and in Python 3.5
to Python 3.7, the number of max workers is five times the number of cores, or 20
(in Python 3.8, this number was changed to &lt;code&gt;min(32, os.cpu_count() + 4)&lt;/code&gt;). We
realized we shouldn&amp;rsquo;t be using the default &lt;code&gt;ThreadExecutor&lt;/code&gt;, and instead we
should provide a &lt;code&gt;ThreadExecutor&lt;/code&gt; with an adjusted number of &lt;code&gt;max_workers&lt;/code&gt;
based on the configured parallelism value. That way, when users run
&lt;code&gt;pulumi up --parallel&lt;/code&gt;, which issues an upper bound on parallel resource
construction, the &lt;code&gt;ThreadExecutor&lt;/code&gt; will respect that bound. We
&lt;a href="https://github.com/pulumi/pulumi/pull/11122"&gt;merged a fix&lt;/a&gt;
that would plumb the value of &lt;code&gt;--parallel&lt;/code&gt; through to a custom &lt;code&gt;ThreadExecutor&lt;/code&gt;
and measured the impact this change had on the performance of our benchmark.&lt;/p&gt;
&lt;h2 id="experimental-setup"&gt;Experimental Setup&lt;/h2&gt;
&lt;p&gt;We designed and implemented two independent experiments to evaluate this change.
The first experiment measures how well the patched Python runtime stacks up
against the control group, Pulumi Python without the patch. The second experiment
compares Pulumi Python to Pulumi TypeScript using the same benchmark ported
to TypeScript. We used the awesome benchmarking tool
&lt;a href="https://github.com/sharkdp/hyperfine"&gt;hyperfine&lt;/a&gt; to record wall clock time as
our indicator of performance.&lt;/p&gt;
&lt;p&gt;The experiments ran overnight on a 2021 MacBook Pro with 32GB RAM, the M1 chip,
and 10 cores. Experimental code is
&lt;a href="https://github.com/pulumi/python-concurrency-experiments/tags"&gt;available on GitHub&lt;/a&gt;,
and release tags pin the version of the code used for each experiment.
We also made an effort to run the experiments on a quiet machine connected
to power. For all experiment groups, &lt;code&gt;--parallel&lt;/code&gt; was unset, translating to
unbounded parallelism.&lt;/p&gt;
&lt;p&gt;Before between samples, we ran &lt;code&gt;pulumi destroy –yes&lt;/code&gt; to ensure a fresh
environment. Hyperfine measures shell startup time and subtracts the value
before final measurements are recorded to more precisely represent the true
cost of execution. All groups collected 20 samples each. We also discard
&lt;code&gt;stderr&lt;/code&gt; and &lt;code&gt;stdout&lt;/code&gt; to reduce noise associated with logging to a tty, but
we do record the status code of each command so can show they executed successfully.&lt;/p&gt;
&lt;h2 id="python-pre--and-post-patch"&gt;Python: Pre- and Post-patch&lt;/h2&gt;
&lt;p&gt;This experiment compares the performance of Pulumi Python before and after
the patch was applied. The control group used Pulumi v3.43.1, while the
experimental group used Pulumi v3.44.3. The primary difference between these
two groups is that a fix was introduced for a Python runtime concurrency bug
as part of v3.44.0. Both groups use the same benchmark program, which created
100 AWS SNS and 100 AWS SQS resources in parallel, as described earlier. Only
the version of the Pulumi CLI is different between groups.&lt;/p&gt;
&lt;h3 id="control-vs-fix"&gt;&lt;a href="https://app.warp.dev/block/rk7fFf2jn2iKXYcIXwhZ8F"&gt;Control vs. Fix&lt;/a&gt;&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Group&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Mean&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Standard Deviation&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Control&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;222.232 s&lt;/td&gt;
&lt;td&gt;0.908 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Experimental&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;70.189 s&lt;/td&gt;
&lt;td&gt;1.497 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; The &lt;strong&gt;Experimental Group&lt;/strong&gt; ran 3.17 ± 0.07 times faster than the &lt;strong&gt;Control Group&lt;/strong&gt;, accounting for a +300% speedup in performance. Running Welch T-Test indicated statical significance (p = 2.93e-59, α=0.05).&lt;/p&gt;
&lt;h2 id="python-vs-typescript"&gt;Python vs. TypeScript&lt;/h2&gt;
&lt;p&gt;After seeing very promising results from the first experiment, we wanted to
determine just how promising these results were. We decided to compare Pulumi
Python to Pulumi TypeScript to see if this fix had narrowed the gap in
performance between the two runtimes. We ported the Python program to TypeScript:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="kr"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt; &lt;span class="kr"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;@pulumi/pulumi&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="kr"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="kr"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;@pulumi/aws&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// SQS
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`pulumi-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// SQS
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`pulumi-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sqs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For this experiment, we fixed the version of the CLI to v3.44.3, which included
the patch to the Python runtime. Here are the result.&lt;/p&gt;
&lt;h3 id="typescript-vs-python"&gt;&lt;a href="https://app.warp.dev/block/rk7fFf2jn2iKXYcIXwhZ8F"&gt;TypeScript vs. Python&lt;/a&gt;&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Group&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Mean&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Standard Deviation&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Python&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;70.975 s&lt;/td&gt;
&lt;td&gt;0.909 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;73.741 s&lt;/td&gt;
&lt;td&gt;1.574 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Summary:&lt;/strong&gt; The &lt;strong&gt;Python Group&lt;/strong&gt; performed the best and ran 1.04 ± 0.03 times
faster than the &lt;strong&gt;TypeScript Group&lt;/strong&gt;. This accounts for a 4% difference in
performance. A second T-Test indicated statical significance
(p = 1.4e-07, α=0.05). Not only did Python close the gap with TypeScript,
but it is also now marginally faster than its Node.js competitor.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s rare to have a small PR result in such a massive performance increase,
but when it happens, we want to shout it from the rooftops. This change, which
shipped last year in v3.44.3, does not require Python users to opt-in; their programs
are now faster. This patch has closed the gap with the Node.js runtime.
Users can now expect highly parallel Pulumi programs to run in a similar
amount of time between either language.&lt;/p&gt;
&lt;h2 id="artifacts"&gt;Artifacts&lt;/h2&gt;
&lt;p&gt;You can check out the artifacts of the experiments
&lt;a href="https://github.com/pulumi/python-concurrency-experiments/tags"&gt;on GitHub&lt;/a&gt;,
including the source code.&lt;/p&gt;
&lt;p&gt;Here are some useful links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/pulumi/python-concurrency-experiments/tags"&gt;The GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pulumi/python-concurrency-experiments/releases/tag/parallelism"&gt;Artifacts from the first experiment&lt;/a&gt;, &amp;ldquo;Control vs. Fix&amp;rdquo; or &amp;ldquo;Pre- and Post-patch&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.warp.dev/block/F6KkbWHvDVWLwtYFKq08Q2"&gt;More statistics&lt;/a&gt; about the first experiment.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pulumi/python-concurrency-experiments/releases/tag/TypeScript-vs-Python"&gt;Artifacts from the second experiment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://app.warp.dev/block/gspCIKn10y9bEvZDMWHe4Q"&gt;More statistics&lt;/a&gt; about the second experiment.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/docs/intro/concepts/how-pulumi-works/"&gt;Pulumi Internals&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><author>Justin Van Patten</author><author>Robbie McKinstry</author><category>performance</category><category>platform</category><category>engineering</category></item><item><title>Achieving Amazing Performance in the Pulumi CLI</title><link>https://www.pulumi.com/blog/amazing-performance/</link><pubDate>Thu, 26 Jan 2023 00:00:00 +0000</pubDate><guid>https://www.pulumi.com/blog/amazing-performance/</guid><description>
&lt;img src="https://www.pulumi.com/images/generated/blog/amazing-performance/index.png" /&gt;
&lt;p&gt;This is the first post in a series about performance optimizations we&amp;rsquo;ve made to the Pulumi CLI. Over the last six months at Pulumi, the Platform Team has been working on a project we call &amp;ldquo;Amazing Performance.&amp;rdquo; Amazing Performance is a new initiative to improve the throughput and latency of the Pulumi CLI not only for power users but for everyone. By the end of June 2022, we assembled a list of issues containing both high-value improvements requiring a sizable investment and low-hanging fruit for quick wins. The full list, including the items we have yet to tackle, is contained in &lt;a href="https://github.com/pulumi/pulumi/issues/11598"&gt;a tracking issue on GitHub&lt;/a&gt;. This blog series will cover the highlights.&lt;/p&gt;
&lt;p&gt;This post has two sections. First, we&amp;rsquo;ll describe the tools we built previously to track performance. Secondly, we&amp;rsquo;ll recap a few quick wins. Future posts will detail our major wins as part of the Amazing Performance initiative.&lt;/p&gt;
&lt;h2 id="measuring-and-tracking-performance"&gt;Measuring and Tracking Performance&lt;/h2&gt;
&lt;p&gt;The Amazing Performance initiative didn&amp;rsquo;t come from a vacuum. We&amp;rsquo;ve been working to understand and improve the performance of the Pulumi CLI for years. Much of the earlier effort has focused on gaining performance &lt;em&gt;insights&lt;/em&gt; – building an understanding of where our performance pains were the sharpest. The two most impactful tools at our disposal for understanding performance pains are our analytics dashboard and our OpenTracing support.&lt;/p&gt;
&lt;h3 id="analytics-dashboard"&gt;Analytics Dashboard&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;ve built an analytics dashboard and monitoring system to let us know when performance dips. This system has three components: a suite of benchmarks, a data warehouse for analytics, and an alerting system.&lt;/p&gt;
&lt;p&gt;&lt;img src="architecture.svg" alt="Performancing Monitoring Architecture"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Benchmark Suite:&lt;/strong&gt; We&amp;rsquo;ve written a suite of benchmarks measuring different performance characteristics of the CLI. Each benchmark answers a simple question: &amp;ldquo;How long does the CLI take to run when the plan has no changes?&amp;rdquo; or &amp;ldquo;How long does the CLI take to run an empty plan?&amp;rdquo; In addition to these simple questions, we also have a few benchmarks to stress test creating many resources. We&amp;rsquo;ve implemented these benchmarks in Go, JavaScript, TypeScript, C#, and Python. Each night, we exercise the benchmarks, capturing samples. Once execution is complete, our nightly job uploads the results to a serverless function which stores them in our data warehouse.&lt;/p&gt;
&lt;p&gt;&lt;img src="analytics-dashboard.jpg" alt="A snippet of our analytics dashboard"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Warehouse Analytics:&lt;/strong&gt; We use Metabase to query and analyze data in our data warehouse. Pulumi engineers can log into Metabase and view our performance dashboard, which charts the nightly data as a time series, where each data point is the average of that night&amp;rsquo;s samples.&lt;/p&gt;
&lt;p&gt;Plotting the chart as a line graph allows us to identify any performance dips visually. Sometimes the data from our nightly runs are noisy, so it can take a few nights before we can attribute a change in the runtime to a change in the code. In the absence of noise, the line graph allows us to pinpoint the day the regression was introduced so we can leaf through the pull requests merged that day. More often, there&amp;rsquo;s enough noise in the data that we go through a few days of PRs to find the regression.&lt;/p&gt;
&lt;p&gt;Shortly after shipping the performance dashboard internally, we used the initial numbers we observed to establish a service-level objective (SLO).&lt;/p&gt;
&lt;p&gt;Each language has an SLO for each of the benchmarks implemented in that language. Our goal for the SLO is to ensure that we set a &amp;ldquo;do not breach&amp;rdquo; expectation across the Platform Core team. We want everyone on the team to know when performance slips beyond an acceptable level.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Alerting:&lt;/strong&gt; What good is an SLO if it&amp;rsquo;s not observable? Whether the breach comes from a major regression or creeps up over time, any time a benchmark violates an SLO, a Slackbot alerts the Platform Core team that performance has slowed beyond an acceptable level. One objective we had for the Amazing Performance initiative was to ensure there were no alerts by the end of Q3 2022, which we achieved.&lt;/p&gt;
&lt;p&gt;&lt;img src="metabot.jpg" alt="Screenshot of a Slack alert from a Metabase SLO breach"&gt;&lt;/p&gt;
&lt;p&gt;Lastly, we established the accuracy of these benchmarks as part of Amazing Performance. We compared the benchmark results we collected in the nightly job with numbers observed on our local laptops and found they were similar. Consequently, we believe our benchmarks are a good predictor of an actual developer&amp;rsquo;s experience.&lt;/p&gt;
&lt;h3 id="tracing-support"&gt;Tracing Support&lt;/h3&gt;
&lt;p&gt;If you ever encounter a particularly sluggish Pulumi program and want to know what is taking so long, you can use &lt;a href="https://www.pulumi.com/docs/support/troubleshooting/#tracing"&gt;Pulumi&amp;rsquo;s tracing support&lt;/a&gt; to look into the details. When run with the &lt;code&gt;--tracing&lt;/code&gt; flag, Pulumi will capture an &lt;a href="https://opentracing.io/"&gt;OpenTracing&lt;/a&gt; trace of the program&amp;rsquo;s execution, giving you an idea of where time is spent. We regularly trace programs large and small to see how we can shave off time.&lt;/p&gt;
&lt;p&gt;Pulumi can send traces to a Zipkin server, or aggregate them to a local file and let you view them offline. If you provide an HTTP URI to the &lt;code&gt;--tracing&lt;/code&gt; flag, then Pulumi assumes that URI points to a web server that accepts Zipkin-format traces, and it will send all tracing data there. If you provide a file URI instead, Pulumi spins up a local Zipkin server to which child processes send their traces. Once execution is complete, Pulumi aggregates the traces into a single local file and spins down the Zipkin server. You can run &lt;code&gt;PULUMI_DEBUG_COMMANDS=1 pulumi view-trace ./up.trace&lt;/code&gt; to view the trace in an embedded web UI.&lt;/p&gt;
&lt;p&gt;&lt;img src="appdash.jpg" alt="An image of a Pulumi trace"&gt;&lt;/p&gt;
&lt;p&gt;Not all spans are exceptionally well-named, and there are blind spots in our traces. However, traces are typically a reliable way to subdivide the program execution, so hotspots are easily identifiable. In the future, we plan to migrate to OpenTelemetry and improve our trace coverage. (Feel free to get in touch on the &lt;a href="https://slack.pulumi.com/"&gt;Community Slack&lt;/a&gt; if you want to help with this effort!)&lt;/p&gt;
&lt;h2 id="quick-wins-for-performance-gains"&gt;Quick Wins for Performance Gains&lt;/h2&gt;
&lt;p&gt;Some of the work we completed for Amazing Performance targeted simple, isolated changes to make the system a little snappier. The rest of this blog details two quick examples.&lt;/p&gt;
&lt;h3 id="skipping-a-slow-import"&gt;Skipping a Slow Import&lt;/h3&gt;
&lt;p&gt;We cut 300ms in boot time a class of programs by dynamically importing TypeScript.&lt;/p&gt;
&lt;p&gt;When Pulumi evaluates NodeJS programs, it compiles TypeScript into JavaScript using &lt;a href="https://www.npmjs.com/package/ts-node"&gt;TS-Node&lt;/a&gt;. Consequently, Pulumi bundles the TypeScript compiler into its NodeJS runtime so TS-Node can perform the compilation. However, we noticed JavaScript programs were importing the TypeScript compiler even when unused.&lt;/p&gt;
&lt;p&gt;Users typically hit this bug in one of two ways. Some users choose to write Pulumi programs in pure JavaScript. Pulumi was needlessly importing the TypeScript compiler, even when there was nothing to compile.&lt;/p&gt;
&lt;p&gt;The second way a user might encounter this bug is in a more advanced deployment pipeline. One common advanced pattern in CI environments or with &lt;a href="https://www.pulumi.com/docs/using-pulumi/automation-api/"&gt;Automation API&lt;/a&gt; is to typecheck Pulumi programs separately from execution. CI will enforce typechecking during pull requests to guarantee that all code hitting the repository&amp;rsquo;s main branch is valid. Once it&amp;rsquo;s typechecked and merged, the Pulumi program can precompile the TypeScript into JavaScript before execution. Finally, when it comes time to execute the Pulumi program, the deployment pipeline will run the compiled JavaScript, which is known to be well-typed. The motivation for precompiling comes from using your own preprocessor. Some customers want to ditch TS-Node for compilation and prefer to use &lt;a href="https://swc.rs/"&gt;SWC&lt;/a&gt;, &lt;a href="https://esbuild.github.io/"&gt;ESBuild&lt;/a&gt;, or other options for increased performance, or for consistency with the rest of their codebase.&lt;/p&gt;
&lt;p&gt;In both scenarios, importing TypeScript when it&amp;rsquo;s not used results in a 300ms slowdown. In &lt;a href="https://github.com/pulumi/pulumi/pull/10214"&gt;a small PR&lt;/a&gt;, we detect cases that don&amp;rsquo;t require TypeScript and dynamically import it only when needed.&lt;/p&gt;
&lt;h3 id="timely-lease-renewal"&gt;Timely Lease Renewal&lt;/h3&gt;
&lt;p&gt;Another quick win involved the renewal of a new lease, which resulted in another 120ms shaved off of boot time. When the Pulumi CLI kicks off a preview, projects that use the &lt;a href="https://www.pulumi.com/docs/iac/concepts/state-and-backends/#pulumi-service-backend"&gt;Service backend&lt;/a&gt; are authenticated with the Service so the CLI can fetch the current state. During authentication, the CLI exchanges user credentials for a short-lived access token. This access token is valid for a fixed amount of time, called a lease, after which they expire. The CLI can renew a lease by making another request to the Service to extend the duration for which the lease is valid.&lt;/p&gt;
&lt;p&gt;We noticed that the first time the CLI renewed the lease, it blocked further execution until the renewal was complete, introducing an unnecessary slowdown of 120ms. &lt;a href="https://github.com/pulumi/pulumi/pull/10462"&gt;The fix&lt;/a&gt; was to remove this renewal from the critical path by running it in the backend. By starting the HTTP call on a background thread and only blocking if the token is needed but not renewed, we&amp;rsquo;ve eliminated the 120ms slowdown.&lt;/p&gt;
&lt;p&gt;Since this bug affects anyone using the Pulumi Service backend, most users will benefit from this improvement. It&amp;rsquo;s small, &lt;a href="https://link.springer.com/chapter/10.1007/978-3-319-58475-1_4"&gt;but it&amp;rsquo;s noticeable&lt;/a&gt; to most users. Program start-up will feel slightly more snappy.&lt;/p&gt;
&lt;h2 id="stay-tuned"&gt;Stay Tuned!&lt;/h2&gt;
&lt;p&gt;This post covered tools we built before Amazing Performance and a few smaller changes we made as part of the initiative. Future posts will describe some of the larger advances we made to drive Pulumi performance ever forward! Keep an eye out for our next post in the series!&lt;/p&gt;</description><author>Robbie McKinstry</author><category>performance</category><category>platform</category><category>engineering</category></item><item><title>Node.js Native Binary Compilation Using vercel/pkg</title><link>https://www.pulumi.com/blog/nodejs-binaries-with-pkg/</link><pubDate>Tue, 13 Sep 2022 00:00:00 +0000</pubDate><guid>https://www.pulumi.com/blog/nodejs-binaries-with-pkg/</guid><description>
&lt;img src="https://www.pulumi.com/images/generated/blog/nodejs-binaries-with-pkg/index.png" /&gt;
&lt;p&gt;In Pulumi&amp;rsquo;s engineering department, we often build and distribute tools as native binaries to avoid the need for additional dependencies on user machines. Most of these tools are written in Go, which has good support for building self-contained binaries that target modern operating systems.&lt;/p&gt;
&lt;p&gt;While other Pulumi-supported languages like Node.js, Python, and .NET require additional runtime dependencies, it&amp;rsquo;s possible to bundle dependencies with the program. In this article, we&amp;rsquo;ll show you how to do that for a Node.js program.&lt;/p&gt;
&lt;p&gt;This is a problem that can be solved using &lt;a href="https://github.com/vercel/pkg"&gt;vercel/pkg&lt;/a&gt; command line tool. Here&amp;rsquo;s the summary from their readme.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This command line interface enables you to package your Node.js project into an executable that can be run even on devices without Node.js installed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Let&amp;rsquo;s take a look at how to use &lt;code&gt;pkg&lt;/code&gt; and some issues we encountered on the way.&lt;/p&gt;
&lt;h2 id="setting-up-pkg"&gt;Setting up &lt;code&gt;pkg&lt;/code&gt;&lt;/h2&gt;
&lt;h3 id="1---install"&gt;1 - Install&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pkg&lt;/code&gt; is distributed as an npm package which can be installed into your “devDependencies” using:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npm install -D pkg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;yarn add -D pkg
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or run without installing with npx:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;npx pkg &lt;span class="o"&gt;[&lt;/span&gt;args&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2---set-bin-in-packagejson"&gt;2 - Set &lt;code&gt;bin&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pkg&lt;/code&gt; will use &lt;a href="https://docs.npmjs.com/cli/v6/configuring-npm/package-json#bin"&gt;the bin field from your package.json&lt;/a&gt; to find the entry point so you just have to specify a path to the directory containing your &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;my-program&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;bin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;bin/index.js&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="err"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If no target is specified, then a set of defaults will be chosen for you. If the output path is not specified, &lt;code&gt;pkg&lt;/code&gt; will infer the name from the package.json “name” field and write to the current working directory.&lt;/p&gt;
&lt;h3 id="3---execute"&gt;3 - Execute&lt;/h3&gt;
&lt;p&gt;The main inputs that &lt;code&gt;pkg&lt;/code&gt; needs is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The entry point to your program for packaging&lt;/li&gt;
&lt;li&gt;The target machine to build for&lt;/li&gt;
&lt;li&gt;The output path to write the finished binary&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s an example of building the project in the current directory using node v18 for macOS ARM architectures:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# pkg [options] &amp;lt;input&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# -t, --targets comma-separated list of targets&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# -o, --output output file name or template for several files&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pkg -t node18-macos-arm64 -o bin/my-program .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It&amp;rsquo;s possible to specify multiple targets in a comma-separated list to build them all at the same time, but it does come with the limitation where the output file names follow a fixed pattern. Instead, we chose to just run the &lt;code&gt;pkg&lt;/code&gt; command multiple times with different arguments from our makefile in parallel as this fits with our existing workflows well.&lt;/p&gt;
&lt;h2 id="issues-encountered"&gt;Issues encountered&lt;/h2&gt;
&lt;h3 id="inspector-not-available"&gt;“Inspector Not Available”&lt;/h3&gt;
&lt;p&gt;As soon as we started executing the provider we started seeing some interesting warnings printed to the console stating “Inspector is not available” (here’s our &lt;a href="https://github.com/pulumi/pulumi-awsx/issues/848"&gt;tracking issue&lt;/a&gt; and some &lt;a href="https://github.com/vercel/pkg/issues/93"&gt;&lt;code&gt;pkg&lt;/code&gt; discussion&lt;/a&gt;). This is because we use the Node.js Inspector API as part of our automatic closure serialization, however this is not available by default when packaging with &lt;code&gt;pkg&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pkg&lt;/code&gt; provides an option to fix this by building your own base image with custom Node.js flags set to enable debugging. However, on investigation, these issues were caused by the Pulumi Typescript SDK creating and caching an inspector instance at the point of being imported even though we never actually call this code in our plugin. Therefore, we opted to make this eager singleton creation to be lazy – only created on first use, as we’re not using in our providers at this time.&lt;/p&gt;
&lt;h3 id="unrunnable-macos-arm-binaries"&gt;Unrunnable MacOS ARM binaries&lt;/h3&gt;
&lt;p&gt;When trying to use the binaries produced by our CI, we found that &lt;a href="https://github.com/pulumi/pulumi-awsx/issues/850"&gt;the binaries weren’t runnable on MacOS ARM architectures&lt;/a&gt; - and were forcibly killed by the operating system.&lt;/p&gt;
&lt;p&gt;This led me down quite a bit of a rabbit hole investigating signing of binaries, but it was actually resolved by simply installing the ‘ldid’ tool on our linux CI environments. The ldid source is available via &lt;a href="git://git.saurik.com/ldid.git"&gt;git.saurik.com/ldid&lt;/a&gt; and binaries are available from various sources. Our solution was to use the &lt;a href="https://github.com/marketplace/actions/install-ldid"&gt;“Install Ldid” GitHub Action&lt;/a&gt; to install the ldid binary and add it to the PATH in our CI workflow. My learning here is that sometimes reading the warnings in logs more carefully can save you lots of time!&lt;/p&gt;
&lt;h3 id="static-binaries"&gt;Static Binaries&lt;/h3&gt;
&lt;p&gt;One adjustment to our configuration came from a &lt;a href="https://github.com/pulumi/pulumi-awsx/pull/862"&gt;community contribution by @afreakk&lt;/a&gt; where the provider was being used in a nixos environment. Nixos adds the requirement for all binaries to be static rather than dynamic - so there’s no requirement for the operating system to dynamically map link functions from system libraries at runtime. Statically compiled programs sometimes result in a larger size, but avoid any possible issues with different versions of the libraries it depends on.&lt;/p&gt;
&lt;p&gt;To resolve this issue, it’s as simple as changing the ‘linux’ targets to ‘linuxstatic’. E.g. &lt;code&gt;node18-linux-amd64&lt;/code&gt; becomes &lt;code&gt;node18-linuxstatic-amd64&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="multi-platform-builds-with-a-makefile"&gt;Multi-platform builds with a makefile&lt;/h2&gt;
&lt;p&gt;We use makefiles to build our providers, so here&amp;rsquo;s a brief outline of how we build for multiple platforms using GNU Make.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-make" data-lang="make"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Set the correct pkg TARGET for each binary we build
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# when building for linux-amd64, set the pkg target to node18-linuxstatic-x64
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;bin/linux-amd64/my-program&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; := &lt;span class="n"&gt;node&lt;/span&gt;18-&lt;span class="n"&gt;linuxstatic&lt;/span&gt;-&lt;span class="n"&gt;x&lt;/span&gt;64
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# output binary file ^ ^ variable ^ pkg target
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;bin/linux-arm64/my-program&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; := &lt;span class="n"&gt;node&lt;/span&gt;18-&lt;span class="n"&gt;linuxstatic&lt;/span&gt;-&lt;span class="n"&gt;arm&lt;/span&gt;64
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;bin/darwin-amd64/my-program&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; := &lt;span class="n"&gt;node&lt;/span&gt;18-&lt;span class="n"&gt;macos&lt;/span&gt;-&lt;span class="n"&gt;x&lt;/span&gt;64
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;bin/darwin-arm64/my-program&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; := &lt;span class="n"&gt;node&lt;/span&gt;18-&lt;span class="n"&gt;macos&lt;/span&gt;-&lt;span class="n"&gt;arm&lt;/span&gt;64
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;bin/windows-amd64/my-program.exe&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; := &lt;span class="n"&gt;node&lt;/span&gt;18-&lt;span class="n"&gt;win&lt;/span&gt;-&lt;span class="n"&gt;x&lt;/span&gt;64
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Wildcard rule to build any of binary outputs
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# &amp;#34;To build any bin file, ensure node_modules are up to date...&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;bin/%&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;node_modules&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# &amp;#34;... then run pkg for actual output name &amp;amp; target&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; yarn run pkg . --target &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; --output &lt;span class="nv"&gt;$@&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# &amp;#34;TARGET&amp;#34; is the variable defined above, depending on the output&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# &amp;#34;$@&amp;#34; is the current makefile target - e.g. &amp;#39;bin/linux-amd64/my-program&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Running `make bins` will build all the listed outputs
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;bins&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bin&lt;/span&gt;/&lt;span class="n"&gt;linux&lt;/span&gt;-&lt;span class="n"&gt;amd&lt;/span&gt;64/&lt;span class="n"&gt;my&lt;/span&gt;-&lt;span class="n"&gt;program&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;bins&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bin&lt;/span&gt;/&lt;span class="n"&gt;linux&lt;/span&gt;-&lt;span class="n"&gt;arm&lt;/span&gt;64/&lt;span class="n"&gt;my&lt;/span&gt;-&lt;span class="n"&gt;program&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;bins&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bin&lt;/span&gt;/&lt;span class="n"&gt;darwin&lt;/span&gt;-&lt;span class="n"&gt;amd&lt;/span&gt;64/&lt;span class="n"&gt;my&lt;/span&gt;-&lt;span class="n"&gt;program&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;bins&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bin&lt;/span&gt;/&lt;span class="n"&gt;darwin&lt;/span&gt;-&lt;span class="n"&gt;arm&lt;/span&gt;64/&lt;span class="n"&gt;my&lt;/span&gt;-&lt;span class="n"&gt;program&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;bins&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bin&lt;/span&gt;/&lt;span class="n"&gt;windows&lt;/span&gt;-&lt;span class="n"&gt;amd&lt;/span&gt;64/&lt;span class="n"&gt;my&lt;/span&gt;-&lt;span class="n"&gt;program&lt;/span&gt;.&lt;span class="n"&gt;exe&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Let Make know that `bins` doesn&amp;#39;t really exist (is phony) - it&amp;#39;s just a helpful shortcut
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bins&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In summary the above code does the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define each output file we need
&lt;ul&gt;
&lt;li&gt;Set the correct &lt;code&gt;pkg&lt;/code&gt; &lt;code&gt;TARGET&lt;/code&gt; for each output&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Define a rule for building any binary output:
&lt;ul&gt;
&lt;li&gt;Ensure module_modules are up-to-date (this is another make target not shown here).&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;pkg&lt;/code&gt; with the &lt;code&gt;$TARGET&lt;/code&gt; set in (1) and the name of the current output (&lt;code&gt;$@&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Define &lt;code&gt;bins&lt;/code&gt; &amp;ldquo;phony target&amp;rdquo; which builds all listed bins.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="note note-tip"&gt;
&lt;div class="icon-and-line"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" class="ph-icon ph-icon--fill" fill="currentColor" aria-hidden="true" focusable="false"&gt;&lt;use href="https://www.pulumi.com/icons/sprite.74fadd1b94bae866bccf29a780f184a71c5cfc34c8677be70da8fe2ab0309b9e.svg#p-lightbulb-fill"/&gt;&lt;/svg&gt;
&lt;div class="line"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="content"&gt;
&lt;p&gt;&lt;strong&gt;You might also like:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/benchmarking-python-performance/"&gt;
Benchmarking Python Performance
&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/blog/amazing-performance/"&gt;
Achieving Amazing Performance in the Pulumi CLI
&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="recap"&gt;Recap&lt;/h2&gt;
&lt;p&gt;I hope this gives you a good overview of the vercel/pkg tool and how you can use it to create standalone programs using Node.js.&lt;/p&gt;
&lt;h3 id="historical-note"&gt;Historical Note&lt;/h3&gt;
&lt;p&gt;We used to do a similar process using the &lt;a href="https://github.com/nexe/nexe"&gt;nexe project&lt;/a&gt;, but there’s been no releases since 2017 and therefore no support for newer versions of Node.js and we therefore consider this package as unmaintained at this point in time.&lt;/p&gt;</description><author>Daniel Bradley</author><category>engineering</category><category>nodejs</category><category>publishing</category><category>providers</category></item></channel></rss>