<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Christian Nunciato</title>
    <description>The latest articles on Forem by Christian Nunciato (@cnunciato).</description>
    <link>https://forem.com/cnunciato</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F552729%2F55955a5b-72aa-4bd0-bdfe-de43d84afb27.jpeg</url>
      <title>Forem: Christian Nunciato</title>
      <link>https://forem.com/cnunciato</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/cnunciato"/>
    <language>en</language>
    <item>
      <title>CDKTF is deprecated: What's next for your team?</title>
      <dc:creator>Christian Nunciato</dc:creator>
      <pubDate>Thu, 18 Dec 2025 20:36:56 +0000</pubDate>
      <link>https://forem.com/pulumi/cdktf-is-deprecated-whats-next-for-your-team-4joa</link>
      <guid>https://forem.com/pulumi/cdktf-is-deprecated-whats-next-for-your-team-4joa</guid>
      <description>&lt;p&gt;In July, 2020, CDK for Terraform (CDKTF) was introduced, and last week, on December 10, it was officially deprecated. Support for CDKTF has stopped, the &lt;a href="https://github.com/cdktf" rel="noopener noreferrer"&gt;organization&lt;/a&gt; and &lt;a href="https://github.com/hashicorp/terraform-cdk" rel="noopener noreferrer"&gt;repository&lt;/a&gt; have been archived, and HashiCorp/IBM will no longer be updating or maintaining it, leaving a lot of teams out there without a clear path forward.&lt;/p&gt;

&lt;p&gt;For most teams, that means it's time to start looking for a replacement.&lt;/p&gt;

&lt;p&gt;It's an unfortunate situation to suddenly find yourself in as a user of CDKTF, but you do have options, and Pulumi is one of them. In this post, we'll help you understand what those options are, how Pulumi fits into them, and what it'd look like to migrate your CDKTF projects to Pulumi.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the options?
&lt;/h2&gt;

&lt;p&gt;Teams migrating away from CDKTF generally have three options:&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Fall back to HCL
&lt;/h3&gt;

&lt;p&gt;HashiCorp's official recommendation is to export your projects to HashiCorp Configuration Language (HCL) and manage them with Terraform. CDKTF even has a command that makes this fairly simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cdktf synth &lt;span class="nt"&gt;--hcl&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, if you're using CDKTF, you probably chose it specifically to avoid HCL. So while possible, this probably isn't the choice most teams would make unless they had to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Migrate to AWS CDK
&lt;/h3&gt;

&lt;p&gt;If your team is all-in on AWS, another option would be to migrate to AWS CDK. It's widely used, officially supported, the programming model is similar to CDKTF's, and both CDK and CDKTF transpile to an intermediate format (CloudFormation YAML and Terraform JSON, respectively) that gets passed on to their underlying tools for deployment.&lt;/p&gt;

&lt;p&gt;But while their programming and deployment models are conceptually similar, their resource models and APIs are entirely different. Here's the code for an S3 bucket written in AWS CDK, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-s3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-bucket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;bucketName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-example-bucket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;versioned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;publicReadAccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the code for a similarly configured bucket in CDKTF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;S3Bucket&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cdktf/provider-aws/lib/s3-bucket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;S3Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-bucket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-example-bucket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;versioning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;acl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;private&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how different these APIs are — and this is just one simple resource with only a few properties; imagine having to rewrite dozens or hundreds of them. Beyond that, there's also the problem of state: How would you go about translating the contents of a Terraform state file containing hundreds of resources into the equivalent CloudFormation YAML or JSON?&lt;/p&gt;

&lt;p&gt;Despite their surface similarities, CDKTF and AWS CDK have little in common. Migration would essentially mean a ground-up rewrite that'd also leave you without the multi-cloud support you already have with CDKTF. For most teams, that makes this option a practical non-starter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: Migrate to Pulumi
&lt;/h3&gt;

&lt;p&gt;This is where we should acknowledge our obvious bias — but we genuinely believe that for most users of CDKTF, Pulumi really is the simplest and most broadly compatible way forward.&lt;/p&gt;

&lt;p&gt;Like CDKTF, Pulumi lets you build and manage your infrastructure with general-purpose languages like TypeScript, Python, Go, C#, and Java, and it supports organizing your code into higher-level abstractions called &lt;a href="https://pulumi.com/docs/iac/concepts/components/" rel="noopener noreferrer"&gt;&lt;em&gt;components&lt;/em&gt;&lt;/a&gt;, which you can think of like CDKTF constructs. Both organize cloud resources into &lt;a href="https://pulumi.com/docs/iac/concepts/stacks/" rel="noopener noreferrer"&gt;&lt;em&gt;stacks&lt;/em&gt;&lt;/a&gt; (think &lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;prod&lt;/code&gt;), and both track &lt;a href="https://pulumi.com/docs/iac/concepts/state-and-backends/" rel="noopener noreferrer"&gt;deployment state&lt;/a&gt; similarly, with local, remote, and cloud-hosted options available.&lt;/p&gt;

&lt;p&gt;Many of Pulumi's most popular &lt;a href="https://pulumi.com/docs/iac/concepts/resources/providers/" rel="noopener noreferrer"&gt;providers&lt;/a&gt; (e.g., the AWS provider) are also built from open-source Terraform schemas, which means their resource models will be nearly identical to what you're used to with CDKTF. Here's what an S3 bucket looks like in Pulumi, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@pulumi/aws&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-bucket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-example-bucket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;versioning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;acl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;private&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use &lt;a href="https://pulumi.com/docs/iac/get-started/terraform/terraform-providers/" rel="noopener noreferrer"&gt;any Terraform provider&lt;/a&gt; with Pulumi, and you can even &lt;a href="https://pulumi.com/docs/iac/guides/migration/migrating-to-pulumi/from-terraform/#using-terraform-modules-directly" rel="noopener noreferrer"&gt;reference Terraform modules directly&lt;/a&gt; from within your Pulumi code.&lt;/p&gt;

&lt;p&gt;Pulumi is also different from CDKTF in several ways. One is that rather than transpile your source code to a format like JSON as CDKTF does (and then deploying it separately later), Pulumi uses its own declarative deployment engine that resolves the resource graph at runtime and provisions cloud resources directly, which is much faster and more flexible. You can learn more about the deployment model in &lt;a href="https://pulumi.com/docs/iac/concepts/how-pulumi-works/" rel="noopener noreferrer"&gt;How Pulumi Works&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Given the API similarities, the support for all Terraform providers and modules, the ability to &lt;a href="https://pulumi.com/docs/iac/guides/migration/#coexistence" rel="noopener noreferrer"&gt;coexist&lt;/a&gt; alongside Terraform-managed projects, and the built-in support for conversion (which we'll cover next), we think Pulumi is the best option for most teams looking to migrate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What migrating to Pulumi looks like
&lt;/h2&gt;

&lt;p&gt;Migrating a CDKTF project to Pulumi generally happens in three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Conversion&lt;/strong&gt;, which translates your CDKTF code into a new Pulumi program&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import&lt;/strong&gt;, which reads the contents of your CDKTF state into a new Pulumi stack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactoring&lt;/strong&gt;, which brings the code in the new program into alignment with the stack's currently deployed resources&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Conversion and import
&lt;/h3&gt;

&lt;p&gt;Migration starts with exporting your CDKTF project to HCL with &lt;code&gt;cdktf synth&lt;/code&gt;. From there, Pulumi's built-in &lt;a href="https://pulumi.com/docs/iac/cli/commands/pulumi_convert/" rel="noopener noreferrer"&gt;&lt;code&gt;convert&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://pulumi.com/docs/iac/cli/commands/pulumi_import/" rel="noopener noreferrer"&gt;&lt;code&gt;import&lt;/code&gt;&lt;/a&gt; commands handle creating the new program and importing your state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Export your project to HCL.&lt;/span&gt;
cdktf synth &lt;span class="nt"&gt;--hcl&lt;/span&gt;

&lt;span class="c"&gt;# Convert the HCL into a new Pulumi project.&lt;/span&gt;
pulumi convert &lt;span class="nt"&gt;--from&lt;/span&gt; terraform &lt;span class="nt"&gt;--language&lt;/span&gt; typescript

&lt;span class="c"&gt;# Create a new Pulumi stack.&lt;/span&gt;
pulumi stack init dev

&lt;span class="c"&gt;# Import your CDKTF stack's resources into your new Pulumi stack.&lt;/span&gt;
pulumi import &lt;span class="nt"&gt;--from&lt;/span&gt; terraform ./terraform.dev.tfstate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The converter automatically translates Terraform input variables, data sources, resources, and outputs into their Pulumi equivalents. You can read more about how this works in &lt;a href="https://pulumi.com/docs/iac/guides/migration/migrating-to-pulumi/from-terraform/#converting-terraform-hcl-to-pulumi" rel="noopener noreferrer"&gt;Converting Terraform HCL to Pulumi&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Refactoring
&lt;/h3&gt;

&lt;p&gt;Once you've imported your state, you'll often have to make some adjustments to the code to bring it in line with the new Pulumi stack. For instance, &lt;code&gt;pulumi import&lt;/code&gt; marks new resources &lt;a href="https://pulumi.com/docs/iac/concepts/resources/options/protect/" rel="noopener noreferrer"&gt;protected&lt;/a&gt; by default, to prevent them from being accidentally deleted — but since the code produced by &lt;code&gt;pulumi convert&lt;/code&gt; doesn't include the &lt;code&gt;protect&lt;/code&gt; resource option, you'll need to add it yourself. Fortunately the import step also emits code that you can copy into your program to make this process a little easier.&lt;/p&gt;

&lt;p&gt;Refactoring can get a bit more complicated when custom logic and higher-level abstractions are involved, as fidelity to the original CDKTF code is often lost in the translation to HCL. In these situations, having the help of an LLM to recapture that original logic or translate your CDKTF constructs into Pulumi components can be a big time-saver.&lt;/p&gt;

&lt;h2&gt;
  
  
  An end-to-end example
&lt;/h2&gt;

&lt;p&gt;The best way to get a feel for how this works, though, is to try it yourself.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/pulumi/cdktf-to-pulumi-example" rel="noopener noreferrer"&gt;&lt;strong&gt;pulumi/cdktf-to-pulumi-example&lt;/strong&gt;&lt;/a&gt; repository on GitHub contains a CDKTF project with multiple stacks written in TypeScript, along with a guide that walks you through the process of migrating that project to Pulumi. The guide covers everything we've discussed here so far, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Converting the CDKTF project into a new Pulumi project&lt;/li&gt;
&lt;li&gt;Importing its actively running resources into Pulumi stacks&lt;/li&gt;
&lt;li&gt;Modifying the generated code to align with imported state&lt;/li&gt;
&lt;li&gt;Performing an initial deployment with Pulumi to complete the migration process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The walkthrough takes only a few minutes to complete, and it's a great way to stand up an example of your own to get more familiar with Pulumi.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;If you’re moving on from CDKTF, there are a few possible paths forward. For teams that want to keep using real languages and avoid a ground-up rewrite, Pulumi offers the clearest way forward.&lt;/p&gt;

&lt;p&gt;To learn more about how Pulumi works, how it differs from CDKTF and from Terraform, how to handle additional conversion scenarios, and more, we recommend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Diving into &lt;a href="https://pulumi.com/docs/iac/concepts/" rel="noopener noreferrer"&gt;the Pulumi docs&lt;/a&gt; to get familiar with core concepts and features of the platform&lt;/li&gt;
&lt;li&gt;Reading &lt;a href="https://pulumi.com/docs/iac/guides/migration/migrating-to-pulumi/from-terraform/" rel="noopener noreferrer"&gt;Migrating from Terraform or CDKTF to Pulumi&lt;/a&gt; for more detailed, Terraform-specific migration guidance&lt;/li&gt;
&lt;li&gt;Joining us in the &lt;a href="https://slack.pulumi.com/" rel="noopener noreferrer"&gt;Pulumi Community Slack&lt;/a&gt; to ask questions and learn from others who've successfully made the leap from Terraform and CDKTF to Pulumi&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And of course, &lt;a href="https://pulumi.com/contact/" rel="noopener noreferrer"&gt;feel free to reach out&lt;/a&gt;! We'd love to help in any way we can.&lt;/p&gt;

</description>
      <category>cdktf</category>
      <category>terraform</category>
      <category>iac</category>
      <category>pulumi</category>
    </item>
    <item>
      <title>Agentic CI with Buildkite: Three practical examples</title>
      <dc:creator>Christian Nunciato</dc:creator>
      <pubDate>Wed, 03 Dec 2025 01:45:09 +0000</pubDate>
      <link>https://forem.com/cnunciato/agentic-ci-with-buildkite-three-practical-examples-12bk</link>
      <guid>https://forem.com/cnunciato/agentic-ci-with-buildkite-three-practical-examples-12bk</guid>
      <description>&lt;p&gt;If you read our previous post about &lt;a href="https://buildkite.com/resources/blog/what-ai-is-teaching-us-about-ci" rel="noopener noreferrer"&gt;how AI is reshaping CI&lt;/a&gt;, then you're aware of the new building blocks we recently added to the Buildkite platform. We're calling these building blocks our agentic workflow components — composable primitives designed to give platform teams the tools they need to bring AI-assisted processes into their CI/CD workflows.&lt;/p&gt;

&lt;p&gt;In this post, we'll show you how to use those components, with three simple-but-practical examples that use AI agents to solve common real-world problems, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reviewing GitHub pull requests&lt;/li&gt;
&lt;li&gt;Automatically fixing broken PR builds&lt;/li&gt;
&lt;li&gt;Generating first-draft PRs based on Linear issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each example is backed by a fully functioning GitHub repository containing a Buildkite pipeline template that you can fork, set up, run, and easily adapt to the needs of your team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recapping the components
&lt;/h2&gt;

&lt;p&gt;Before diving into the examples themselves, let's quickly recap the workflow components we introduced in our previous post, as you'll be using them in the examples that follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://buildkite.com/resources/blog/whats-new-in-the-buildkite-mcp-server/" rel="noopener noreferrer"&gt;The Buildkite MCP server&lt;/a&gt;, which gives your agents fine-grained access to the Buildkite REST API through our specialized MCP server tools&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://buildkite.com/docs/apis/model-providers" rel="noopener noreferrer"&gt;Buildkite model providers&lt;/a&gt;, which let you connect to popular frontier models like Anthropic's Claude Code and others directly through Buildkite, either by using your own API credentials or with a hosted, Buildkite-managed key&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://buildkite.com/docs/apis/webhooks/incoming/pipeline-triggers" rel="noopener noreferrer"&gt;Buildkite pipeline triggers&lt;/a&gt;, inbound webhooks that you can use to invoke a Buildkite pipeline in response to any external event with an HTTP request, with first-class support for popular services like GitHub and Linear&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://buildkite.com/docs/pipelines/configure/dynamic-pipelines/sdk" rel="noopener noreferrer"&gt;The Buildkite SDK&lt;/a&gt;, which lets you compose and generate pipeline definitions &lt;a href="https://buildkite.com/docs/pipelines/configure/dynamic-pipelines" rel="noopener noreferrer"&gt;dynamically at runtime&lt;/a&gt; using general-purpose programming languages like JavaScript, TypeScript, Python, Go, and Ruby&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://buildkite.com/resources/plugins/" rel="noopener noreferrer"&gt;A collection of plugins&lt;/a&gt; powered by Claude, Codex, Amazon Bedrock, and others that make it easy to use LLMs to &lt;a href="https://buildkite.com/docs/agent/v3/cli-annotate" rel="noopener noreferrer"&gt;annotate&lt;/a&gt; your CI jobs with rich build summaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together, these building blocks, in combination with the broader Buildkite platform, let you build flexible and adaptive workflows that bring AI agents into your CI/CD process on your own terms. Let's see how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1: GitHub code-review bot
&lt;/h2&gt;

&lt;p&gt;We all know how important code review is to the delivery process — but it also takes time, and with AI agents producing more code than ever for us humans to review, well, let's just say we need all the help we can get to keep up with it. Having a capable AI agent at hand to help out with the occasional first-pass code review (whether for yourself or someone else) can be a significant time-saver — not to mention a way to surface the kinds of non-obvious bugs that can easily sneak past human reviewers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Get the code
&lt;/h3&gt;

&lt;p&gt;The code for this example is available on GitHub at &lt;a href="https://github.com/buildkite-agentic-examples/github-code-review-bot" rel="noopener noreferrer"&gt;buildkite-agentic-examples/github-code-review-bot&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;This first example configures a Buildkite pipeline to listen for GitHub PR events (specifically labeled events) using a Buildkite &lt;a href="https://buildkite.com/docs/apis/webhooks/incoming/pipeline-triggers" rel="noopener noreferrer"&gt;pipeline trigger&lt;/a&gt;. Triggers are essentially inbound webhooks — unique URLs tied to a specific pipeline — which means you can &lt;a href="https://docs.github.com/en/webhooks/using-webhooks/creating-webhooks" rel="noopener noreferrer"&gt;add them to any GitHub repository&lt;/a&gt; to have GitHub run the pipeline in response to certain events. This particular pipeline runs an AI agent (Claude Code by default) that evaluates a PR and submits a code review as a GitHub comment.&lt;/p&gt;

&lt;p&gt;All three of the examples we're covering in this post follow the same general pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A label gets applied, triggering a Buildkite pipeline.&lt;/li&gt;
&lt;li&gt;The pipeline runs a script that parses and validates the webhook payload.&lt;/li&gt;
&lt;li&gt;The script appends a step to the running Buildkite pipeline to spawn an AI agent.&lt;/li&gt;
&lt;li&gt;The agent completes the task.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most interesting work happens in the handler script, &lt;code&gt;scripts/handler.ts&lt;/code&gt;, a Node.js program written in TypeScript that uses the Buildkite and GitHub SDKs to run Claude Code with a task-specific prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pipeline&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@buildkite/buildkite-sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Octokit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;octokit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="c1"&gt;// Generate the pipeline with the Buildkite SDK.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateCodeReviewPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webhookPullRequestUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agentBuildUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pipeline&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenArgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`PullRequestURL=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;webhookPullRequestUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`AgentBuildURL=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;agentBuildUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addStep&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:buildkite: Reviewing the code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nf"&gt;runAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tokenArgs&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;buildkite-agentic-example-tools:latest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mount-checkout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mount-buildkite-agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;environment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="c1"&gt;//...&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TRIGGER_ON_LABEL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MODEL_PROVIDER&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toYAML&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Fetch the incoming payload from Buildkite.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildkiteAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;meta-data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;buildkite:webhook&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="c1"&gt;// Exit unless the payload has a label matching the one we're listening for.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;labelName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&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 class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;labelName&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TRIGGER_ON_LABEL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Label is not '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TRIGGER_ON_LABEL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', exiting`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="c1"&gt;// Generate and upload a new pipeline step to run the AI agent.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pipelineYaml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateCodeReviewPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pullRequestUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uploadProcess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;buildkite-agent pipeline upload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pipelineYaml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The step runs Claude in a Docker container (using the Buildkite &lt;a href="https://buildkite.com/resources/plugins/buildkite-plugins/docker-buildkite-plugin/" rel="noopener noreferrer"&gt;docker plugin&lt;/a&gt;) that includes only the tools Claude needs for this particular task: Node.js, the GitHub CLI, the &lt;a href="https://buildkite.com/docs/apis/mcp-server#types-of-mcp-servers" rel="noopener noreferrer"&gt;local version&lt;/a&gt; of the Buildkite MCP server, the Claude Code CLI, and the necessary scripts, prompts, and environment variables. Running the agent in a container like this isn't technically necessary — it just adds some additional isolation and safety over running Claude Code directly on your filesystem. (The container itself is built and tagged locally at runtime using an &lt;a href="https://buildkite.com/docs/agent/v3/hooks" rel="noopener noreferrer"&gt;agent lifecycle hook&lt;/a&gt;. See &lt;code&gt;.buildkite/hooks/post-checkout&lt;/code&gt; for details.)&lt;/p&gt;

&lt;p&gt;Inside the container, Claude clones the PR's GitHub repository, checks out the PR branch, analyzes the change, and posts a review back to the PR as a comment, annotating the Buildkite build (using the MCP server's annotation tooling) as it goes.&lt;/p&gt;

&lt;p&gt;All settings — including the model provider and label — are configurable in &lt;code&gt;.buildkite/pipeline.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GITHUB_TOKEN&lt;/span&gt;
  &lt;span class="na"&gt;BUILDKITE_API_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;API_TOKEN_BUILDKITE&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;GITHUB_CLI_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.83.0"&lt;/span&gt;
  &lt;span class="na"&gt;BUILDKITE_MCP_SERVER_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.7.3"&lt;/span&gt;
  &lt;span class="na"&gt;TRIGGER_ON_LABEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;buildkite-review"&lt;/span&gt;
  &lt;span class="na"&gt;MODEL_PROVIDER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anthropic"&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:node:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Generate&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pipeline"&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;# Generate and upload the pipeline to handle the webhook.&lt;/span&gt;
      &lt;span class="s"&gt;echo "--- :webhook: Run the webhook handler"&lt;/span&gt;
      &lt;span class="s"&gt;npm install &amp;amp;&amp;amp; npm run build&lt;/span&gt;
      &lt;span class="s"&gt;node dist/handler&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One last thing to point out (as it's easy to miss) is that the &lt;code&gt;claude&lt;/code&gt; CLI isn't running in the usual way, communicating with the Anthropic API as it normally would. Instead, it's using a Buildkite-managed &lt;a href="https://buildkite.com/docs/apis/model-providers/anthropic" rel="noopener noreferrer"&gt;model-provider endpoint&lt;/a&gt; that proxies the Anthropic API for you. Open up &lt;code&gt;scripts/claude.sh&lt;/code&gt; and you'll see that the two environment variables Claude uses to configure its backend are being set using Buildkite environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;

&lt;span class="c"&gt;# Set up Buildkite Hosted Models&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUILDKITE_AGENT_ENDPOINT&lt;/span&gt;&lt;span class="s2"&gt;/ai/anthropic"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUILDKITE_AGENT_ACCESS_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# ...&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"--- :robot_face: Starting Claude Code"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$prompt&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;--mcp-config&lt;/span&gt; mcp.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These two values are being set with &lt;a href="https://buildkite.com/docs/pipelines/configure/environment-variables" rel="noopener noreferrer"&gt;pipeline environment variables&lt;/a&gt; applied automatically by Buildkite at runtime. Together, they give you a seamless way to use Claude — or any supported model provider — in your pipelines without having to provide (and then expose, and manage) your own Anthropic credentials. See the &lt;a href="https://buildkite.com/docs/apis/model-providers" rel="noopener noreferrer"&gt;model providers documentation&lt;/a&gt; for details.&lt;/p&gt;

&lt;p&gt;The following diagram shows the operative components and how they come together:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2huyw25l3e3eguaujusb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2huyw25l3e3eguaujusb.png" alt="A diagram showing the components of the github-github-code-review-bot example." width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/buildkite-agentic-examples/github-code-review-bot" rel="noopener noreferrer"&gt;See the example's README&lt;/a&gt; for complete setup instructions and configuration details.&lt;/p&gt;

&lt;p&gt;Once you're up and running (which takes only a few minutes — the README walks you through it), you'll have a fully functioning code-review bot that you can call up by adding a &lt;code&gt;buildkite-review&lt;/code&gt; label to any pull request — and a reusable Buildkite pipeline and workflow that you can adapt as you like to the needs of your team.&lt;/p&gt;

&lt;p&gt;Now let's have a look at the next example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 2: GitHub PR build fixer
&lt;/h2&gt;

&lt;p&gt;I don't know about you, but every now and then, I'll have a PR build fail, and I'll have no idea why. Often the failure has something to do with a linter — although the logs may or may not make this clear, as linters are famous for failing in totally obscure ways that make no sense at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdxapiakf60ldzc796plo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdxapiakf60ldzc796plo.gif" alt="An animated GIF of Tom Cruise looking very confused" width="426" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;☝️ Me, when this happens.&lt;/p&gt;

&lt;p&gt;In situations like these (and in other, more complex ones), it can be nice to be able to reach for an AI agent for a hand in diagnosing and fixing the issue.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Get the code
&lt;/h3&gt;

&lt;p&gt;The code for this example is available on GitHub at &lt;a href="https://github.com/buildkite-agentic-examples/github-pr-build-fixer" rel="noopener noreferrer"&gt;buildkite-agentic-examples/github-pr-build-fixer&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;This example follows the same high-level pattern as before:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You create a new Buildkite pipeline and trigger with the example, adding the trigger as a GitHub webhook to whichever repositories you'd like Claude to fix for you.&lt;/li&gt;
&lt;li&gt;When a PR build fails, you add the appropriate label to it — in this case, &lt;code&gt;buildkite-fix&lt;/code&gt; (although as before, this is configurable in &lt;code&gt;./buildkite/pipeline.yml&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;GitHub invokes the webhook, triggering the pipeline, which evaluates the webhook payload and adds a step that runs Claude in a Docker container.&lt;/li&gt;
&lt;li&gt;Claude uses the Buildkite MCP server to query the logs, finds the root cause, clones the repo, implements a fix, and pushes a new branch containing the fix to GitHub.&lt;/li&gt;
&lt;li&gt;Claude makes a new PR (on the original, still-broken PR), waits for the Buildkite build to pass (iterating if necessary), and posts a summary comment on the original PR explaining the fix.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the PR looks good, you click the merge button, pull in the fix, and get on with your day.&lt;/p&gt;

&lt;p&gt;Here's how this looks architecturally:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flwknug66j5ikgscbev3a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flwknug66j5ikgscbev3a.png" alt="A diagram showing the components of the github-pr-build-fixer example." width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Full setup and configuration details are &lt;a href="https://github.com/buildkite-agentic-examples/github-pr-build-fixer" rel="noopener noreferrer"&gt;in the README&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'd also encourage you to have a look at the prompt for this example, which you'll find at &lt;a href="https://github.com/buildkite-agentic-examples/github-pr-build-fixer/blob/main/prompts/user.md" rel="noopener noreferrer"&gt;prompts/user.md&lt;/a&gt;. You may want to make some adjustments to it to align with whatever standards or guidelines you use on your team.&lt;/p&gt;

&lt;p&gt;Now let's turn to our last example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 3: Linear issue handler
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://linear.app/" rel="noopener noreferrer"&gt;Linear&lt;/a&gt; has become an incredibly popular tool for managing software projects. (We even use it ourselves here at Buildkite.) If you've used Linear, you know how it works, but if you haven't, it's essentially an issue tracker — much like Jira, Trello, GitHub Issues, or others you've likely used before.&lt;/p&gt;

&lt;p&gt;Issue trackers are at the center of how most of us organize our work, and at any given moment, I might have dozens or hundreds of issues assigned to me personally. Some are simple and well-defined — dependency bumps, framework upgrades. Others are much more complex, requiring deeper analysis to come up with more substantive estimates.&lt;/p&gt;

&lt;p&gt;Fortunately, both types can often be handled (in whole or in part) with a good LLM, given a good-enough issue description and prompt as a starting point.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  Get the code
&lt;/h3&gt;

&lt;p&gt;The code for this example is available on GitHub at &lt;a href="https://github.com/buildkite-agentic-examples/linear-issue-handler" rel="noopener noreferrer"&gt;buildkite-agentic-examples/linear-issue-handler&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;Hopefully by now, the pattern is clear: You set up a pipeline, give it a trigger, wire up the trigger to be invoked in response to some external event, and your agent of choice takes care of the rest. Here, the goal is to have Linear kick off a Buildkite pipeline in response to an issue-label event.&lt;/p&gt;

&lt;p&gt;Once the trigger is set up in your Linear project — &lt;a href="https://github.com/buildkite-agentic-examples/github-pr-build-fixer" rel="noopener noreferrer"&gt;see the README&lt;/a&gt; for how to do that — whenever you add the &lt;code&gt;buildkite-analyze&lt;/code&gt; label to an issue, the pipeline runs &lt;code&gt;scripts/handler.ts&lt;/code&gt;, extracts the issue details, discerns from the issue description which GitHub organization and repository the issue refers to, and generates a pipeline step that runs Claude as it does in the other examples.&lt;/p&gt;

&lt;p&gt;In this one, Claude analyzes the codebase as well, and makes a judgment call (much as a human would) on the level of complexity involved. For simpler issues, Claude will go ahead and implement a fix, opening a PR and commenting back on the Linear issue with a summary and link to it. For more complex issues, it'll render its findings, suggest possible approaches, etc., and post an analysis back to the Linear issue instead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjd09qt6chhr2sxbzozfq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjd09qt6chhr2sxbzozfq.png" alt="A diagram showing the components of the linear-issue-handler example." width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As with the other examples, all of the work performed by the LLM is done within a Docker container, which includes the Linear CLI here to give Claude more controlled access to Linear functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes these work
&lt;/h2&gt;

&lt;p&gt;Stepping back, these simple yet practical examples all reflect the core principles and foundational tools we think teams need in order to bring agentic processes into their CI workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Composable primitives that enable flexible, adaptive CI workflows&lt;/li&gt;
&lt;li&gt;Built-in access to frontier models with minimal configuration&lt;/li&gt;
&lt;li&gt;Pipeline triggers that let you extend your Buildkite pipelines to external services more easily&lt;/li&gt;
&lt;li&gt;A highly configurable, performance-focused MCP server that gives your agents fine-grained access to Buildkite resources&lt;/li&gt;
&lt;li&gt;A multi-language SDK that lets you build pipelines intelligently and dynamically at runtime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;... and more. Each component is individually useful, but taken together they enable agentic workflows that can branch, adapt, fan out, etc., based on what the agent discovers at runtime — something statically written YAML-based pipelines can't really do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;The best way to get a feel for these components is to try them yourself. All you need are a &lt;a href="https://buildkite.com/signup" rel="noopener noreferrer"&gt;Buildkite account&lt;/a&gt;, a &lt;a href="https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28" rel="noopener noreferrer"&gt;GitHub personal access token&lt;/a&gt;, a &lt;a href="https://buildkite.com/docs/apis/managing-api-tokens" rel="noopener noreferrer"&gt;Buildkite API access token&lt;/a&gt;, and a &lt;a href="https://linear.app/docs/api-and-webhooks#api-keys" rel="noopener noreferrer"&gt;Linear API key&lt;/a&gt; for the Linear example, and each one takes only a few minutes to set up.&lt;/p&gt;

&lt;p&gt;Follow the instructions in their respective READMEs to get started:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/buildkite-agentic-examples/github-code-review-bot" rel="noopener noreferrer"&gt;buildkite-agentic-examples/github-code-review-bot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/buildkite-agentic-examples/github-pr-build-fixer" rel="noopener noreferrer"&gt;buildkite-agentic-examples/github-pr-build-fixer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/buildkite-agentic-examples/linear-issue-handler" rel="noopener noreferrer"&gt;buildkite-agentic-examples/linear-issue-handler&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To learn more about the each of these components, see their documentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://buildkite.com/docs/apis/mcp-server" rel="noopener noreferrer"&gt;Buildkite MCP server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://buildkite.com/docs/apis/model-providers" rel="noopener noreferrer"&gt;Buildkite model providers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://buildkite.com/docs/apis/webhooks/incoming/pipeline-triggers" rel="noopener noreferrer"&gt;Buildkite pipeline triggers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://buildkite.com/docs/pipelines/configure/dynamic-pipelines/sdk" rel="noopener noreferrer"&gt;Buildkite SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://buildkite.com/resources/plugins" rel="noopener noreferrer"&gt;Buildkite plugins&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have fun! We can't wait to see what you build. 🙌&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>ai</category>
      <category>llm</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Automating Jenkins with Configuration as Code (JCasC)</title>
      <dc:creator>Christian Nunciato</dc:creator>
      <pubDate>Fri, 20 Jun 2025 17:56:41 +0000</pubDate>
      <link>https://forem.com/cnunciato/automating-jenkins-with-configuration-as-code-jcasc-jg</link>
      <guid>https://forem.com/cnunciato/automating-jenkins-with-configuration-as-code-jcasc-jg</guid>
      <description>&lt;p&gt;When you've spent as much time as I have in the world of infrastructure-as-code (IaC), you develop a bit of an allergy to anything that looks like click-ops.&lt;/p&gt;

&lt;p&gt;There's nothing inherently wrong with click-ops, of course; sometimes it's just what you need. But in general, if I'm standing up infrastructure, especially on a team, I'm going to reach for something like &lt;a href="https://www.pulumi.com/docs/" rel="noopener noreferrer"&gt;Pulumi&lt;/a&gt;, &lt;a href="https://developer.hashicorp.com/terraform" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt;, or &lt;a href="https://docs.ansible.com/ansible/latest/index.html" rel="noopener noreferrer"&gt;Ansible&lt;/a&gt; to help. To me, that's just how it's done — there's no better way to get the reliable, repeatable results I need without being able to run an automated, code-driven process to produce them.&lt;/p&gt;

&lt;p&gt;Which is why I was surprised to learn that in the Jenkins world, clicking around in the UI to configure the controller, plugins, agents, etc., isn't just common — it's still largely the norm.&lt;/p&gt;

&lt;p&gt;Fortunately for IaC-heads like me, there's an answer. &lt;a href="https://www.jenkins.io/projects/jcasc/" rel="noopener noreferrer"&gt;Jenkins Configuration as Code (JCasC)&lt;/a&gt; is a Jenkins plugin that lets you define your Jenkins configuration entirely in code, specifically YAML, that can be version-controlled, tested, and applied just as you would with any other IaC tool. And it's actually quite simple to use — once you understand the basics.&lt;/p&gt;

&lt;p&gt;So in this hands-on post, we'll dive into JCasC with an example that sets up a Jenkins controller from scratch, complete with global settings, plugins, inbound agents, and a working pipeline wired up to a GitHub repository, all configured in code. To make it easy to spin up and work on, we'll also use Docker and Docker Compose, but the concepts apply no matter what you're deploying to — whether that's a Kubernetes cluster, a fleet of cloud-hosted VMs, or a stack of Raspberry Pis on your desktop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is JCasC and how does it work?
&lt;/h2&gt;

&lt;p&gt;Like all things Jenkins, JCasC is a plugin. But it's a special one — it's a plugin for managing Jenkins itself, meant to allow you to spin up and configure a Jenkins cluster without having to sign into the Jenkins UI. Chances are, if you can configure it in the UI, you can accomplish the same thing with JCasC in YAML.&lt;/p&gt;

&lt;p&gt;To make it work, you'll need a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;An instance of Jenkins&lt;/strong&gt;. Specifically, you'll need to be able to start an instance of Jenkins somehow, as JCasC configuration is applied on startup. (Containers, system services, and CLI commands are all fine.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A JCasC configuration file&lt;/strong&gt; — a YAML file. You can name this file anything you like, but the convention is to name it &lt;code&gt;jenkins.yaml&lt;/code&gt;. We'll walk through what goes into this file in this post.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;To tell Jenkins where this file is located&lt;/strong&gt; when you start it up. This is generally done with an environment variable. You'll see some examples of this later as well.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Technically that's all there is to it — but if you'd like to install plugins automatically as well (which you almost surely will), you'll also need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A file containing a list of plugins to install&lt;/strong&gt; on the Jenkins controller. You can name this file anything as well, but it's usually called &lt;code&gt;plugins.txt&lt;/code&gt;. Installing plugins this way is immensely valuable in that it not only lets you specify the plugin versions you'd like to install (one of many &lt;a href="https://buildkite.com/resources/blog/best-practices-for-managing-jenkins-plugins/" rel="noopener noreferrer"&gt;Jenkins plugin best practices&lt;/a&gt;), but also track changes to your plugin versions over time, test them before you deploy, and keep them all configured consistently across your organization.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Jenkins plugin CLI&lt;/strong&gt;, which is how you'll install the plugins on that list. There are other ways to do this, but the CLI is the standard.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The beauty of this approach is that it lets you configure not just one instance of Jenkins, but as many as you like, and to keep your production Jenkins clusters stable until you're ready to upgrade them with well-tested replacements.&lt;/p&gt;

&lt;p&gt;All right, enough talk. Let's take a look at the actual files to see how this works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diving into a JCasC example
&lt;/h2&gt;

&lt;p&gt;Start by cloning the example repository, which you'll find on GitHub at &lt;a href="https://github.com/cnunciato/jenkins-jcasc-example" rel="noopener noreferrer"&gt;cnunciato/jenkins-jcasc-example&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh repo clone cnunciato/jenkins-jcasc-example
&lt;span class="nb"&gt;cd &lt;/span&gt;jenkins-jcasc-example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you've done that, you should see the following layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── app # A simple Node.js app that we're using for testing.
│ ├── index.js
│ ├── index.test.js
│ ├── package-lock.json
│ └── Jenkinsfile # The Jenkinsfile that builds and tests the app.
├── Dockerfile.controller # The Dockerfile for the Jenkins controller.
├── Dockerfile.agent # The Dockerfile for the Jenkins agents.
├── docker-compose.yml # The service definitions for the controller and agents.
├── agent.sh # The setup script for the agents.
├── jenkins.yaml # 👈 The JCasC configuration file.
└── plugins.txt # 👈 The list of plugins to be installed on the controller.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are several files here, but the two most important ones are &lt;code&gt;jenkins.yaml&lt;/code&gt; and &lt;code&gt;plugins.txt&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the controller with &lt;code&gt;jenkins.yaml&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;jenkins.yaml&lt;/code&gt; file is what'll define most of your Jenkins controller's configuration, so where you'll spend most of your time. Open that file in your editor of choice, and let's walk through each section to get a sense of what it does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic build settings
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jenkins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NORMAL&lt;/span&gt;
  &lt;span class="na"&gt;numExecutors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These settings — &lt;code&gt;mode: NORMAL&lt;/code&gt; and &lt;code&gt;numExecutors: 0&lt;/code&gt; — tell Jenkins to accept any build job assigned to it and to delegate those jobs to connected agents instead — i.e., to run zero jobs on the controller itself. It's a common best practice for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Running jobs in on agents, as opposed to on the controller, reduces the risk of those jobs affecting the performance stability of the controller (and of exposing sensitive data).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pushing work off to agents keeps the controller's CPU and memory focused on managing the build queue, handling front-end requests, and coordinating workloads rather than competing with them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Agent-only execution makes horizontal scaling more straightforward. To handle more load, you can add more agents without having to upgrade the controller.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The default &lt;code&gt;mode&lt;/code&gt; is &lt;code&gt;NORMAL&lt;/code&gt;, so this particular setting is optional, but in the spirit of configuration as documentation, it's a good idea to be explicit about it so that others understand it's intentional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Provisioning users
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jenkins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="na"&gt;securityRealm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;allowsSignup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JENKINS_ADMIN_USERNAME}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JENKINS_ADMIN_PASSWORD}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JENKINS_AGENT_USERNAME}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JENKINS_AGENT_PASSWORD}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the &lt;code&gt;securityRealm&lt;/code&gt; block tells Jenkins to configure two users on the controller:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;An &lt;code&gt;admin&lt;/code&gt; user, so you can sign into the Jenkins UI without having to copy and paste the generated password from the startup logs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An &lt;code&gt;agent&lt;/code&gt; user to allow build agents to self-register (which you'll see later)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;allowsSignup: false&lt;/code&gt; setting keeps users from being able to create accounts on the controller by visiting &lt;code&gt;${JENKINS_URL}/signup&lt;/code&gt;. Like &lt;code&gt;mode: NORMAL&lt;/code&gt;, it's also the default, so technically optional — but again, a good one to be explicit about as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting roles and permissions
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jenkins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="na"&gt;authorizationStrategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;roleBased&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
            &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Overall/Administer&lt;/span&gt;
            &lt;span class="na"&gt;entries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JENKINS_ADMIN_USERNAME}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agent&lt;/span&gt;
            &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Overall/Read&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Agent/Connect&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Agent/Disconnect&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Agent/Build&lt;/span&gt;
            &lt;span class="na"&gt;entries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${JENKINS_AGENT_USERNAME}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Surprisingly, Jenkins has no support for authorization out of the box; the &lt;code&gt;authorizationStrategy&lt;/code&gt; block has just two modes: &lt;code&gt;unsecured&lt;/code&gt;, meaning anyone with access to the controller can use it without restriction (i.e., everyone's a fully anonymous administrator), and &lt;code&gt;loggedInUsersCanDoAnything&lt;/code&gt;, meaning anyone with an account can act as an administrator. Both are far too permissive to be useful in production, so to secure your controllers properly, you'll need to look to a plugin.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://plugins.jenkins.io/role-strategy/" rel="noopener noreferrer"&gt;role-strategy plugin&lt;/a&gt; is a popular choice — and of course, it's configurable with JCasC. Here, we're using that plugin to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Define an &lt;code&gt;admin&lt;/code&gt; role with full privileges and apply it to the &lt;code&gt;admin&lt;/code&gt; user&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Define an &lt;code&gt;agent&lt;/code&gt; role with narrower privileges (allowing it to connect, self-register, and run builds) and apply it to the &lt;code&gt;agent&lt;/code&gt; user&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To learn more about the role-strategy plugin, &lt;a href="https://plugins.jenkins.io/role-strategy/" rel="noopener noreferrer"&gt;see its documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring agents
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jenkins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;permanent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agent1&lt;/span&gt;
        &lt;span class="na"&gt;launcher&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inbound&lt;/span&gt;
        &lt;span class="na"&gt;remoteFS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/home/jenkins/agent&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;permanent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agent2&lt;/span&gt;
        &lt;span class="na"&gt;launcher&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inbound&lt;/span&gt;
        &lt;span class="na"&gt;remoteFS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/home/jenkins/agent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;nodes&lt;/code&gt; block configures the controller to support two permanently connected Jenkins agents using the &lt;code&gt;inbound&lt;/code&gt; connection type. Here, agents connect to the controller, rather than the other way around, as they would with the &lt;code&gt;ssh&lt;/code&gt; or &lt;code&gt;command&lt;/code&gt; types. It's a flexible pattern that allows agents to spin up, connect, run builds, and terminate without Jenkins needing to be able to reach them directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing build tools
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nodejs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;installations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Node 22.x&lt;/span&gt;
        &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;installSource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;installers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;nodeJSInstaller&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;22.16.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tool&lt;/code&gt; block lets you configure the build tools that you use in your pipelines. This particular block — &lt;code&gt;nodejs&lt;/code&gt; — automates the provisioning of Node.js on all build agents that need it. It tells Jenkins that when an agent connects, the agent should download and install Node version &lt;code&gt;22.16.0&lt;/code&gt; from npm if it hasn't already.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring a pipeline job and GitHub repo
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;multibranchPipelineJob('jenkins-jcasc-example') {&lt;/span&gt;
        &lt;span class="s"&gt;branchSources {&lt;/span&gt;
          &lt;span class="s"&gt;github {&lt;/span&gt;
            &lt;span class="s"&gt;id('jenkins-jcasc-example')&lt;/span&gt;
            &lt;span class="s"&gt;repoOwner('cnunciato')&lt;/span&gt;
            &lt;span class="s"&gt;repository('jenkins-jcasc-example')&lt;/span&gt;
            &lt;span class="s"&gt;scanCredentialsId('github-pat')&lt;/span&gt;
          &lt;span class="s"&gt;}&lt;/span&gt;
        &lt;span class="s"&gt;}&lt;/span&gt;
        &lt;span class="s"&gt;factory {&lt;/span&gt;
          &lt;span class="s"&gt;workflowBranchProjectFactory {&lt;/span&gt;
            &lt;span class="s"&gt;scriptPath('app/Jenkinsfile')&lt;/span&gt;
          &lt;span class="s"&gt;}&lt;/span&gt;
        &lt;span class="s"&gt;}&lt;/span&gt;
        &lt;span class="s"&gt;configure { node -&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;def traits = node / sources / data / 'jenkins.branch.BranchSource' / source / traits&lt;/span&gt;
          &lt;span class="s"&gt;traits &amp;lt;&amp;lt; 'org.jenkinsci.plugins.github__branch__source.BranchDiscoveryTrait' {&lt;/span&gt;
            &lt;span class="s"&gt;strategyId(3)&lt;/span&gt;
          &lt;span class="s"&gt;}&lt;/span&gt;
        &lt;span class="s"&gt;}&lt;/span&gt;
      &lt;span class="s"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;jobs&lt;/code&gt; block uses the Job DSL, a Groovy-based API for creating Jenkins jobs declaratively. Here, we're defining a single multibranch pipeline job that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Fetches this blog post's example repository from GitHub (using credentials we'll define in a moment)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Locates the &lt;code&gt;Jenkinsfile&lt;/code&gt; of the app to be built in the repository (in the &lt;code&gt;app&lt;/code&gt; folder)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configures a branch-discovery trait that builds all branches of the repository — namely strategy &lt;code&gt;3&lt;/code&gt;, a magic number that means "regular and PR branches", where strategies &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;2&lt;/code&gt; mean "exclude PR branches" and "only PR branches," respectively&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuring shared GitHub credentials
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;domainCredentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;usernamePassword&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github-pat&lt;/span&gt;
              &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${GITHUB_USERNAME}"&lt;/span&gt;
              &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${GITHUB_TOKEN}"&lt;/span&gt;
              &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${GITHUB_USERNAME}'s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;personal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;access&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;token"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, while we don't necessarily have to configure GitHub credentials for this example, since the job we've configured pulls from a public repository (and we're only running it locally), in most cases, you'll want to do so, both to be able to access private repositories and to avoid running up against GitHub's fairly restrictive API limits.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;credentials&lt;/code&gt; block defines a shared, reusable Jenkins credential named &lt;code&gt;github-pat&lt;/code&gt; (for personal access token) using environment variables that you specify at startup time. As you likely noticed above, the &lt;code&gt;jenkins-jcasc-example&lt;/code&gt; job uses this credential to fetch the associated repository from GitHub.&lt;/p&gt;

&lt;h3&gt;
  
  
  Discovering more configuration settings
&lt;/h3&gt;

&lt;p&gt;It's important to note that there are many more settings that you can configure with JCasC and &lt;code&gt;jenkins.yaml&lt;/code&gt; than the few we've covered here — and figuring out what they are can be rather challenging (and require a good deal of trial and error). Two suggestions I'll offer to help you save time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Consult the &lt;a href="https://github.com/jenkinsci/configuration-as-code-plugin" rel="noopener noreferrer"&gt;configuration-as-code-plugin&lt;/a&gt; repository&lt;/strong&gt;. Most of the settings you'll be interested in are covered in the &lt;code&gt;demos&lt;/code&gt; folder of that repository, including those of commonly used plugins like the ones we've used here. If you're stuck, have a look around, and there's a good chance you'll find what you're looking for there.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cheat — with the Jenkins UI&lt;/strong&gt;. In addition to letting you write the configuration of a Jenkins controller, the JCasC plugin also allows you to read it — an incredibly handy way to discover settings you didn't know existed. In &lt;strong&gt;Manage Jenkins&lt;/strong&gt; &amp;gt; &lt;strong&gt;Configuration as Code&lt;/strong&gt;, there's a &lt;strong&gt;View Configuration&lt;/strong&gt; button that surfaces everything as one big YAML blob, plugin settings and all, that you can paste right into &lt;code&gt;jenkins.yaml&lt;/code&gt; and apply. If you're not sure how to configure something, try setting it up first with the UI, then click that button to see what changed, and copy out what you need. Having a Docker-based setup like this one makes this approach especially easy.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configuring plugins with &lt;code&gt;plugins.txt&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I won't spend too much time on this topic, since it's technically independent of JCasC (and there's not all that much to it anyway) — but automating the installation of plugins along with the configuration of the server itself is such a common practice alongside JCasC that it's worth covering here as well.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;plugins.txt&lt;/code&gt; file simply contains the names of the plugins you'd like to install on the controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ansicolor # Support for colorized build logs
configuration-as-code # 👈 The core JCasC functionality
github-branch-source # Support for multibranch GitHub
job-dsl # Declarative job definitions
nodejs # Support for Node.js
workflow-aggregator # Core pipelines functionality
workflow-multibranch # Multibranch pipeline features
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The snippet above uses unpinned versions, which implicitly tells Jenkins to fetch and install the latest version of each plugin. This is a fairly risky approach, given how easy it'd be for one or more of these plugins to fall out of compatibility with the Jenkins controller (or with one another) and break your pipeline, so most of the time, you'll want to pin each plugin to a specific version for stability and repeatability.&lt;/p&gt;

&lt;p&gt;You can do this easily by adding the version after the plugin name (which you can get from the &lt;a href="https://plugins.jenkins.io/" rel="noopener noreferrer"&gt;Jenkins Plugins Index&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ansicolor:1.0.6
configuration-as-code:1971.vf9280461ea_89
github-branch-source:1822.v9eec8e5e69e3
job-dsl:1.93
nodejs:1.6.4
role-strategy:777.v4fe2599cb_f48
workflow-aggregator:608.v67378e9d3db_1
workflow-multibranch:806.vb_b_688f609ee9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maintaining this list can be tedious, and there doesn't seem to be a way to specify an acceptable range (only concrete versions), but it's much safer than simply relying on the latest.&lt;/p&gt;

&lt;p&gt;That covers the two files — &lt;code&gt;jenkins.yaml&lt;/code&gt; and &lt;code&gt;plugins.txt&lt;/code&gt; — that govern most of your ability to configure a Jenkins server from scratch.&lt;/p&gt;

&lt;p&gt;You might still be wondering, though: How exactly do you instruct Jenkins to &lt;em&gt;use&lt;/em&gt; these two files?&lt;/p&gt;

&lt;h2&gt;
  
  
  Spinning up the cluster with Docker Compose
&lt;/h2&gt;

&lt;p&gt;The best way to see how this works is to spin it up and have a look under the hood.&lt;/p&gt;

&lt;p&gt;Now open a terminal, and assuming you have Docker installed (and the Docker daemon running), you should be able to bring everything up with a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now browse to &lt;a href="http://localhost:8080" rel="noopener noreferrer"&gt;http://localhost:8080&lt;/a&gt; and sign in with the configured username and password — both of which are &lt;code&gt;admin&lt;/code&gt;, as specified in &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6harr0g0k23pc5abtmhn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6harr0g0k23pc5abtmhn.png" alt="The Jenkins sign-in screen" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can even trigger a build and see the connected agents pick it up and run it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj0um4xnirll2guhnxjdt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj0um4xnirll2guhnxjdt.png" alt="A Jenkins build" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining controller and agent services
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;docker-compose.yml&lt;/code&gt;, you'll see three container definitions: &lt;code&gt;jenkins&lt;/code&gt;, &lt;code&gt;agent1&lt;/code&gt;, and &lt;code&gt;agent2&lt;/code&gt;. There's a bunch of other YAML in there, but it's mostly environment variables and defaults, so I'll assume you know your way around that stuff and just focus on the relevant lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# The controller.&lt;/span&gt;
  &lt;span class="na"&gt;jenkins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile.controller&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;

  &lt;span class="c1"&gt;# An agent.&lt;/span&gt;
  &lt;span class="na"&gt;agent1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile.agent&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;jenkins&lt;/span&gt;

  &lt;span class="c1"&gt;# Another agent.&lt;/span&gt;
  &lt;span class="na"&gt;agent2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile.agent&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;jenkins&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;jenkins&lt;/code&gt; service's referenced Dockerfile, you'll see that it pulls in &lt;code&gt;jenkins.yaml&lt;/code&gt; and &lt;code&gt;plugins.txt&lt;/code&gt;, then runs the &lt;code&gt;jenkins-plugin-cli&lt;/code&gt; (which is included in the Jenkins container image) to install all plugins, baking them into the container image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; jenkins/jenkins:jdk21&lt;/span&gt;

&lt;span class="c"&gt;# Copy in jenkins.yaml and plugins.txt.&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; jenkins.yaml /usr/share/jenkins/ref/jenkins.yaml&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; plugins.txt /usr/share/jenkins/ref/plugins.txt&lt;/span&gt;

&lt;span class="c"&gt;# Install all plugins with jenkins-plugin-cli.&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;jenkins-plugin-cli &lt;span class="nt"&gt;--plugin-file&lt;/span&gt; /usr/share/jenkins/ref/plugins.txt

&lt;span class="c"&gt;# Disable the setup wizard and tell JCasC where the config file is located.&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; JAVA_OPTS="-Djenkins.install.runSetupWizard=false"&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; CASC_JENKINS_CONFIG="/usr/share/jenkins/ref/jenkins.yaml"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last two environment variable settings are the key to making this work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;JAVA_OPTS&lt;/code&gt; tells Jenkins to skip running the setup wizard (you no longer need it — you're rolling with JCasC now)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;CASC_JENKINS_CONFIG&lt;/code&gt; tells the JCasC plugin where to find &lt;code&gt;jenkins.yaml&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From there, Docker runs the default entrypoint to start the Jenkins service.&lt;/p&gt;

&lt;p&gt;In a non-Docker-based environment, it's conceptually the same, just slightly different in that you'll need to fetch the Jenkins package and the Jenkins Plugin Manager individually. On Ubuntu, that'd look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DEBIAN_FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;noninteractive

&lt;span class="c"&gt;# Install Jenkins dependencies.&lt;/span&gt;
apt-get update
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; wget gnupg curl openjdk-21-jdk

&lt;span class="c"&gt;# Install Jenkins.&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | &lt;span class="nb"&gt;tee&lt;/span&gt; /usr/share/keyrings/jenkins-keyring.asc &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/"&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/jenkins.list &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
apt-get update
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; jenkins

&lt;span class="c"&gt;# Make sure jenkins.yaml and plugins.txt and exist.&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-al&lt;/span&gt; /usr/share/jenkins/ref/jenkins.yaml
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-al&lt;/span&gt; /usr/share/jenkins/ref/plugins.txt

&lt;span class="c"&gt;# Download the Jenkins Plugin Installation Manager.&lt;/span&gt;
wget &lt;span class="nt"&gt;-O&lt;/span&gt; /tmp/jenkins-plugin-manager.jar https://github.com/jenkinsci/plugin-installation-manager-tool/releases/download/2.12.13/jenkins-plugin-manager-2.12.13.jar

&lt;span class="c"&gt;# Run the plugin manager to install the plugins listed in plugins.txt.&lt;/span&gt;
java &lt;span class="nt"&gt;-jar&lt;/span&gt; /tmp/jenkins-plugin-manager.jar &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--war&lt;/span&gt; /usr/share/java/jenkins.war &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--plugin-file&lt;/span&gt; /usr/share/jenkins/ref/plugins.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--plugin-download-directory&lt;/span&gt; /var/lib/jenkins/plugins

&lt;span class="c"&gt;# Set required environment variables.&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JENKINS_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/lib/jenkins
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JAVA_OPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-Djenkins.install.runSetupWizard=false"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CASC_JENKINS_CONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/usr/share/jenkins/ref/jenkins.yaml"&lt;/span&gt;

&lt;span class="c"&gt;# Start Jenkins.&lt;/span&gt;
&lt;span class="c"&gt;# systemctl start jenkins # Or just `jenkins`, to start.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either way, that's all there is to it: a &lt;code&gt;jenkins.yaml&lt;/code&gt; file, a &lt;code&gt;plugins.txt&lt;/code&gt; file, a CLI command to load your plugins, and two environment variables, and your Jenkins controller is fully configured — no need to sign into the Jenkins UI to configure a thing.&lt;/p&gt;

&lt;p&gt;Now let's have a last look at how the agent setup and self-registration work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring agent self-registration
&lt;/h2&gt;

&lt;p&gt;If you open the agents' Dockerfile, you'll see that it's built from &lt;code&gt;jenkins/inbound-agent&lt;/code&gt;, but it's using a shell script, &lt;code&gt;agent.sh&lt;/code&gt;, as the entrypoint, rather than the default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; jenkins/inbound-agent:jdk21&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; root&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; jenkins&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; agent.sh /agent.sh&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/agent.sh"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's because in order to connect to the Jenkins controller, an agent needs to supply a secret — and those secrets are generated dynamically (and uniquely for each agent) by the controller. You can view them in the Jenkins UI, and you can copy from the UI into the terminal when you fire up an agent — but who wants to do that? The whole point of JCasC, after all, is to avoid having to click around in the Jenkins UI and paste things into a terminal.&lt;/p&gt;

&lt;p&gt;Fortunately, there's a way to retrieve the generated secret directly from the controller, which is exactly what &lt;code&gt;agent.sh&lt;/code&gt; does. The script calls the &lt;code&gt;jenkins-agent.jnlp&lt;/code&gt; endpoint (using the credentials we specified for the &lt;code&gt;agent&lt;/code&gt; account earlier) and parses the response with &lt;code&gt;sed&lt;/code&gt; to pull out the secret value before setting it as the &lt;code&gt;JENKINS_SECRET&lt;/code&gt; environment variable (along with a few others):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting for Jenkins to start..."&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;15

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Registering '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;JENKINS_AGENT_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' with the controller..."&lt;/span&gt;

&lt;span class="c"&gt;# Retrieve the agent secret from its metadata endpoint.&lt;/span&gt;
&lt;span class="nv"&gt;AGENT_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;JENKINS_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/computer/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;JENKINS_AGENT_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/jenkins-agent.jnlp"&lt;/span&gt;
&lt;span class="nv"&gt;AGENT_METADATA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; GET &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;JENKINS_AGENT_USERNAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;JENKINS_AGENT_PASSWORD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;$AGENT_ENDPOINT&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;AGENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$AGENT_METADATA&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s2"&gt;"s/.*&amp;lt;application-desc&amp;gt;&amp;lt;argument&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;[a-z0-9]*&lt;/span&gt;&lt;span class="se"&gt;\)&lt;/span&gt;&lt;span class="s2"&gt;.*/&lt;/span&gt;&lt;span class="se"&gt;\1\n&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Starting '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;JENKINS_AGENT_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'..."&lt;/span&gt;

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JENKINS_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AGENT_SECRET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JENKINS_AGENT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JENKINS_AGENT_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JENKINS_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JENKINS_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;exec &lt;/span&gt;jenkins-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with that, you're good to go: You now have a fully functioning example of configuring Jenkins completely from scratch, including plugins, self-registering agents, and a Docker-based setup to make development and testing a little less painful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;We've covered a lot, and hopefully we've given you a solid understanding of how JCasC works and how to use it. There's much more to learn — we've just scratched the surface — but what's here should give you a decent foundation to build onto.&lt;/p&gt;

&lt;p&gt;To keep the learning going, you might want to check out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The &lt;a href="https://www.jenkins.io/projects/jcasc/" rel="noopener noreferrer"&gt;Jenkins Configuration as Code sub-project&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;a href="https://www.jenkins.io/doc/book/managing/casc/" rel="noopener noreferrer"&gt;Configuration as Code page&lt;/a&gt; of the Jenkins Handbook&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;a href="https://github.com/jenkinsci/configuration-as-code-plugin" rel="noopener noreferrer"&gt;jenkinsci/configuration-as-code repository&lt;/a&gt; on GitHub — especially the &lt;a href="https://github.com/jenkinsci/configuration-as-code-plugin/tree/master/demos" rel="noopener noreferrer"&gt;demos&lt;/a&gt; folder&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And of course, keep on tinkering! You'll find our example on GitHub at &lt;a href="https://github.com/cnunciato/jenkins-jcasc-example" rel="noopener noreferrer"&gt;cnunciato/jenkins-jcasc-example&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>jenkins</category>
      <category>devops</category>
      <category>cicd</category>
      <category>docker</category>
    </item>
    <item>
      <title>Understanding the SLSA framework</title>
      <dc:creator>Christian Nunciato</dc:creator>
      <pubDate>Fri, 21 Mar 2025 19:30:15 +0000</pubDate>
      <link>https://forem.com/cnunciato/understanding-the-slsa-framework-38ec</link>
      <guid>https://forem.com/cnunciato/understanding-the-slsa-framework-38ec</guid>
      <description>&lt;p&gt;If writing secure software is difficult, securing software build pipelines and supply chains can be even more challenging. The Software Supply Chain Levels for Software Artifacts (&lt;a href="https://slsa.dev/" rel="noopener noreferrer"&gt;SLSA&lt;/a&gt;) has emerged in response to recent large-scale public attacks on digital infrastructure. It provides an industry-wide structured approach to fortifying software supply chains against threats, and is designed to evolve to address new threats as they arise.&lt;/p&gt;

&lt;p&gt;If you’re looking for assurances that the open-source software you’re using is secure, find yourself needing to assess the security of your software vendors, or want to protect your internal systems and software from supply chain attacks, you'll likely want to learn more about SLSA. &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What is SLSA?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Pronounced “salsa”, SLSA is an open-source framework that helps you secure your software delivery pipelines to better protect yourself and your users from serious risks. SLSA is based on security practices developed internally at Google and has been developed in public since 2021. It was initially released as a response to the massive security breach now known as the SolarWinds attack (which we’ll cover below). &lt;/p&gt;

&lt;p&gt;SLSA is stewarded by the Open Source Security Foundation (&lt;a href="https://openssf.org/" rel="noopener noreferrer"&gt;OpenSSF&lt;/a&gt;) and governed by a diverse industry-wide group of specialists. As a relative newcomer on the security scene, its evolving definition targets threats to software supply chains—something not covered well by existing efforts like &lt;a href="https://csrc.nist.gov/projects/ssdf" rel="noopener noreferrer"&gt;NIST&lt;/a&gt;, &lt;a href="https://csrc.nist.gov/projects/ssdf" rel="noopener noreferrer"&gt;SSDF&lt;/a&gt;, and &lt;a href="https://pbom.dev/" rel="noopener noreferrer"&gt;PBOM&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How SLSA works&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;SLSA provides documentation, automated tooling, and in-depth guidelines to help teams evaluate and communicate the security level of software artifacts and their dependencies. &lt;/p&gt;

&lt;p&gt;SLSA advocates for evaluating each software artifact independently; it does not endorse transitive trust. The SLSA level of your dependencies and build tooling helps inform the security of your software artifacts without guaranteeing it. &lt;/p&gt;

&lt;p&gt;Practitioners can self-evaluate or audit others, and you should be able to use SLSA’s language to quickly and effectively communicate about how secure your software is, what you’re expecting from third parties (such as software vendors and OSS), and the security of the software you’re providing to your customers.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;SLSA tracks and levels&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;SLSA evaluations are organized along &lt;em&gt;tracks&lt;/em&gt; and &lt;em&gt;levels&lt;/em&gt;. Each track in SLSA addresses a different aspect of the software supply chain, and levels in a track indicate an artifact’s maturity therein. The currently published version (release 1.0) focuses on the &lt;a href="https://slsa.dev/spec/v1.0/levels#build-track" rel="noopener noreferrer"&gt;“Build” track&lt;/a&gt;, and details four levels, from 0 to 3. &lt;a href="https://slsa.dev/spec/v1.1/future-directions" rel="noopener noreferrer"&gt;Future releases&lt;/a&gt; will introduce additional levels and tracks (under active consideration: "Source" and "Platform Operations").&lt;/p&gt;

&lt;p&gt;&lt;a href="https://slsa.dev/spec/v1.0/levels" rel="noopener noreferrer"&gt;The levels&lt;/a&gt; within the SLSA Build track are designed to classify artifacts based on how secure the build pipeline is, and provide requirements for build systems. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Level 0: No assurances&lt;/strong&gt;. The baseline level for software artifacts with no security measures in place.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 1: Basic security and provenance (protects against accidents)&lt;/strong&gt;. This level focuses on documentation and consistency. Builds are consistent—artifacts only change when their source code changes. Artifacts are tagged with documentation about how they were built (i.e., &lt;em&gt;provenance&lt;/em&gt;), and this documentation is accessible for audits.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 2: Enhanced security enforcement (protects against common attacks)&lt;/strong&gt;. This level focuses on authenticity and preventing post-build artifact tampering. Software artifact provenance should be signed, and builds use tamper-resistant platforms.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 3: Comprehensive security controls (protects against concerted attacks)&lt;/strong&gt;. This level focuses on tamper prevention during the build process. Artifact provenance and documentation should be made hard to forge. Build systems compliant with this level provide advanced protections, such as stringent access controls and a secured and hardened environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A proposed &lt;strong&gt;Level 4&lt;/strong&gt; will likely focus on aspects like the consistency and integrity of build environments and require adopting more complex protective measures such as hardware attestation and reproducible builds.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Core principles&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The Build track levels above embody SLSA’s &lt;a href="https://slsa.dev/spec/v1.0/principles" rel="noopener noreferrer"&gt;three core principles&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust platforms, verify artifacts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Validating platforms is time-consuming and difficult. SLSA encourages adopters to minimize the number of platforms used in a build system and to prefer shared and hosted platforms over self-managed ones. This makes auditing and securing platforms more efficient since platform users can coordinate and share related costs. For artifact verification, SLSA is biased toward automatic and rapid verification, and provides some standard tools and guidelines for the work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust code, not individuals&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Individuals are difficult to vet and audit—and even vetted and trusted individuals can make mistakes, be temporarily compromised, or unintentionally contribute to malicious updates in other ways. SLSA believes it's more efficient to audit, validate, statically analyze, and develop trust in written code. Code changes can be evaluated more efficiently—piecemeal, on a per-change or per-update basis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer attestations over inferences&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Securing and trusting systems is difficult—systems may operate as expected but still produce compromised artifacts. SLSA is built around the idea that it is much more efficient and easy to check each artifact and audit its build process than to attempt to (automatically) infer whether an artifact is secure by auditing and analyzing the systems and people involved in building it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How SLSA is used today&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Companies may use SLSA to assess their internal infrastructure, highlight security status, and identify gaps. Or they might use it to audit software vendors, suppliers, and external dependencies. At a high level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Participation and use of SLSA are voluntary.
&lt;/li&gt;
&lt;li&gt;There isn’t (yet) a formal certification process for SLSA. Companies may self-assess or run audits for each other.
&lt;/li&gt;
&lt;li&gt;OSS software, build platforms, and industry insiders are gradually documenting their compliance levels and other contributions to supply chain security to build trust—e.g., &lt;a href="https://buildkite.com/resources/blog/securing-our-software-a-look-at-continuous-compliance-and-governance-in-ci-cd/" rel="noopener noreferrer"&gt;Buildkite&lt;/a&gt;, &lt;a href="https://edu.chainguard.dev/open-source/slsa/what-is-slsa/" rel="noopener noreferrer"&gt;Chainguard Academy&lt;/a&gt;, &lt;a href="https://adoptium.net/blog/2024/01/slsabuild3-temurin/" rel="noopener noreferrer"&gt;Eclipse Temurin&lt;/a&gt;, &lt;a href="https://tekton.dev/docs/chains/slsa-provenance/" rel="noopener noreferrer"&gt;Tekton&lt;/a&gt;, and &lt;a href="https://www.activestate.com/resources/quick-reads/supply-chain-levels-for-software-artifacts-slsa/" rel="noopener noreferrer"&gt;ActiveState&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Key terms&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;SLSA uses some &lt;a href="https://slsa.dev/spec/v1.0/terminology" rel="noopener noreferrer"&gt;specialized language and terminology&lt;/a&gt;. The most important concepts and terms include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Software artifact&lt;/strong&gt;: A file or bundle resulting from a build process. Examples include binaries, container images, and software packages.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software supply chain&lt;/strong&gt;: All the steps and activities used to make and deliver software artifacts. This includes writing code and using libraries and tools produced by others, code repositories, build systems, package managers, and deployment mechanisms.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditability&lt;/strong&gt;: The capacity to know about, inspect, and verify each step in a supply chain, usually to check compliance with security practices and standards.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attestation&lt;/strong&gt;: Machine-readable and cryptographically signed data associated with a specific software artifact. It usually provides security-related context, such as what the artifact contains, how it was produced, and how it might be used.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provenance&lt;/strong&gt;: An attestation that describes how, where, and by whom an artifact was produced, and which other artifacts were involved in its production.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build process integrity&lt;/strong&gt;: The extent to which a build process hasn't been tampered with.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproducibility&lt;/strong&gt;: The ability to produce identical artifacts from the same source code and build environment.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hermetic builds&lt;/strong&gt;: Builds that are entirely self-contained, consistent, and 100% reproducible. They do not depend on external factors like network access or the state of the build environment.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-person review&lt;/strong&gt;: A process where at least two individuals review—and then verifiably sign off on—change proposals to a system before they are incorporated.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What threats is SLSA designed to address?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;SLSA aims to protect supply chains against a full range of pervasive and sophisticated threats. It can guide your defense from vulnerabilities introduced through dependencies, build system compromises, code injection, and various insider threats. &lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Real-world supply chain attack examples&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Below are some examples of the kinds of attacks SLSA addresses today. (Over time, these will expand.)&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Build process compromise&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;The now-canonical example of a supply chain attack, the &lt;a href="https://www.techtarget.com/whatis/feature/SolarWinds-hack-explained-Everything-you-need-to-know" rel="noopener noreferrer"&gt;SolarWinds hack&lt;/a&gt; from 2020, exposed the systems and data of more than 18,000 public, private, government, military, and other highly sensitive organizations. Attackers compromised the build processes at multiple companies, including SolarWinds, which allowed them to issue a malicious update to SolarWinds’s Orion IT software. The update was quickly installed worldwide and provided hackers with significant systems and data access. This hack prompted the creation of the SLSA framework, and its Level 2 and Level 3 build-process controls are designed to prevent and mitigate future similar attacks.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Malicious artifact injection&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;In an example from 2021, attackers gained access to &lt;a href="https://about.codecov.io/apr-2021-post-mortem/" rel="noopener noreferrer"&gt;Codecov’s software delivery systems&lt;/a&gt; and were able to replace one of Cocodev’s tool offerings with a malicious artifact. Most of Codecov’s 23,000 clients used the tool in automated scripts as part of their software build chains and were compromised in the attack.&lt;/p&gt;

&lt;p&gt;The initial security breach would have been mitigated with the security controls SLSA recommends for build processes. The malicious artifact injection was ultimately detected through a basic provenance check (checking the SHA signature of downloaded software against the expected signature). SLSA formalizes provenance and attestation processes as part of Build Level 2.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Chained dependency and code injection into released artifacts&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;An attack from 2018 called the &lt;a href="https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident" rel="noopener noreferrer"&gt;event-stream incident&lt;/a&gt; is an example of chained dependency and code injection through a compromised open-source software project. &lt;/p&gt;

&lt;p&gt;Hackers tried to steal data and cryptocurrency from the users of an app called Copay. They modified the popular &lt;a href="https://www.npmjs.com/package/event-stream" rel="noopener noreferrer"&gt;&lt;code&gt;event-stream&lt;/code&gt; package&lt;/a&gt; to include a new dependency in its build. Most developers didn’t notice, because the dependency only introduced malware when included in the Copay application. &lt;/p&gt;

&lt;p&gt;This complex attack would be detected and prevented with the &lt;a href="https://slsa.dev/spec/v1.0/verifying-artifacts" rel="noopener noreferrer"&gt;optional recursive check&lt;/a&gt; included with regular SLSA 1.0 audits. It’s a check that explicitly catches chained dependency attacks by recursing through and validating all dependencies.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Malicious code injection and insider tampering&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;In a sophisticated attack from 2024 known as the &lt;a href="https://www.wired.com/story/jia-tan-xz-backdoor/" rel="noopener noreferrer"&gt;XZ Backdoor attack&lt;/a&gt;, state actors created a fake persona and spent four years gaining trust and contributing to a popular open-source compression library called XZ Utils. They added thousands of legitimate contributions and code to many projects before making a camouflaged malicious update.&lt;/p&gt;

&lt;p&gt;SLSA Build Level 3 protections and recursive SLSA audits provide some protection from these kinds of attacks. As a wider portion of the industry audits, reviews, and ensures the OSS tools they use are properly authenticated and attributed, this kind of attack gets harder to perform and to keep hidden. The proposed Build Level 4 would also increase protection from this type of attack, for example by encouraging hermetic builds. (Once code has been reviewed and audited, a hermetic build guarantees it hasn’t been tampered with, no matter who builds it.) Future SLSA versions will require auditable, authenticated, and verified two-person reviews for code changes, targeting attacks like this.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How to get started using SLSA&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;SLSA is designed with ease of adoption in mind. Moving up through Build track levels (and implementing future tracks once they’re released) should feel as minimally invasive as possible. Let’s look at how you might go about adopting SLSA on your own.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Educate and self-evaluate&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;To start adopting SLSA into your software supply chain, take inventory of your software artifacts and build chains. List all the components and artifacts and assign a SLSA level to each one based on its current state. Questions like "How critical is this artifact to our operations?" or "What level of exposure to risk does this artifact have?" can help guide your evaluations and prioritize compliance work to improve SLSA levels for artifacts.&lt;/p&gt;

&lt;p&gt;Once you have an inventory for your systems, you can begin implementing automated verification tools across your codebase. SLSA provides some examples, like the &lt;a href="https://github.com/slsa-framework/slsa-verifier" rel="noopener noreferrer"&gt;SLSA-verifier on GitHub&lt;/a&gt;, that help check compliance with SLSA standards and identify improvements.&lt;/p&gt;

&lt;p&gt;Having verified your artifacts to determine the baseline, you can now set a target SLSA level for each artifact. Your business needs will dictate priorities and naturally lead to a strategic plan with actions to move each artifact to its target level. The SLSA team provides example tools, references, and &lt;a href="https://slsa.dev/how-to/get-started" rel="noopener noreferrer"&gt;guidelines&lt;/a&gt; for how this work can be efficient, quick, automated, and scaled up.&lt;/p&gt;

&lt;p&gt;Implementing SLSA will be a continual effort. Regularly review your progress and watch for &lt;a href="https://slsa.dev/current-activities" rel="noopener noreferrer"&gt;developments&lt;/a&gt; in the specifications. Adapt your security strategy to match the standards as they change, and your software supply chain will remain resistant to emerging threats.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Implement the SLSA Build Track&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Here’s an overview of the kind of work required to implement each level of SLSA’s Build track. For more information, refer to the &lt;a href="https://slsa.dev/how-to/" rel="noopener noreferrer"&gt;how-to guides on the SLSA website&lt;/a&gt;. &lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Level 0&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Do nothing. This level indicates there are no SLSA assurances in place.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Level 1&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Identify your build artifacts, and automatically generate, log, and distribute &lt;a href="https://slsa.dev/spec/v1.0/provenance" rel="noopener noreferrer"&gt;standardized&lt;/a&gt; provenance metadata.
&lt;/li&gt;
&lt;li&gt;Standardize and document all build processes for auditability.
&lt;/li&gt;
&lt;li&gt;Monitor the supply chain to detect inconsistencies.
&lt;/li&gt;
&lt;li&gt;Use tools like GitHub Actions or Buildkite Package Registries, which has &lt;a href="https://buildkite.com/docs/package-registries/security/slsa-provenance" rel="noopener noreferrer"&gt;built-in support for SLSA Build Level 1&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Level 2&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Sign provenance data, attestations, and artifacts with cryptographic signatures for integrity validation and continuous monitoring.
&lt;/li&gt;
&lt;li&gt;Secure and centralize the build process, ensuring auditability and tamper resistance.
&lt;/li&gt;
&lt;li&gt;Consider tools like &lt;a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/dashboard/code-signing-reqs#windows-10-attestation-signed-drivers-for-testing-scenarios" rel="noopener noreferrer"&gt;Windows signed attestation&lt;/a&gt;, &lt;a href="https://pinata.cloud/blog/introducing-the-content-attestation-plugin/" rel="noopener noreferrer"&gt;IPFS Signed Attestation plugin&lt;/a&gt;, and public attestation repos like &lt;a href="https://docs.sigstore.dev/logging/overview/" rel="noopener noreferrer"&gt;Rektor&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Level 3&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Harden build platforms with regular audits and updates, ensuring documentation authenticity.
&lt;/li&gt;
&lt;li&gt;Automate validations and checks to prevent tampering.
&lt;/li&gt;
&lt;li&gt;Continuously audit the build process, follow relevant &lt;a href="https://buildkite.com/docs/agent/v3/securing" rel="noopener noreferrer"&gt;hardening guidelines&lt;/a&gt;, and consider resources like SLSA attestations for toolsets (e.g., for &lt;a href="https://fluxcd.io/flux/security/slsa-assessment/" rel="noopener noreferrer"&gt;flux&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Level 4 and beyond (future)&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Implement hermetic builds, and add audits and attestations to your build frameworks.
&lt;/li&gt;
&lt;li&gt;Clearly define roles, responsibilities, and processes for SLSA enforcement and monitoring.
&lt;/li&gt;
&lt;li&gt;Resources include tools like &lt;a href="https://buildkite.com/docs/pipelines/tutorials/bazel" rel="noopener noreferrer"&gt;Bazel&lt;/a&gt; (for &lt;a href="https://bazel.build/basics/hermeticity" rel="noopener noreferrer"&gt;hermetic builds)&lt;/a&gt; and the &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Kubernetes_Security_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP Kubernetes security cheat sheet&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Keep up to date with SLSA&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;SLSA is maintained and modified in the open, and its administration follows public OpenSSF standards. The team behind SLSA includes a steering committee, specific working groups, and community contributions. You can &lt;a href="https://slsa.dev/community#get-involved" rel="noopener noreferrer"&gt;contribute directly&lt;/a&gt;, and there are many ways to keep up to date with their work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regularly look for changes posted to the authoritative location for SLSA, &lt;a href="https://slsa.dev/" rel="noopener noreferrer"&gt;https://slsa.dev&lt;/a&gt;. Updates are available in both human-readable and machine-accessible formats. (As time of writing, the &lt;a href="https://slsa.dev/spec/v1.1/whats-new" rel="noopener noreferrer"&gt;SLSA v1.1 draft&lt;/a&gt; is available for public review.)
&lt;/li&gt;
&lt;li&gt;Refer to the &lt;a href="https://slsa.dev/blog" rel="noopener noreferrer"&gt;SLSA blog&lt;/a&gt; for regular updates, directions, and feedback requests.
&lt;/li&gt;
&lt;li&gt;For more granular updates, see the &lt;a href="https://docs.google.com/document/d/1PwhekVB1iDpcgCQRNVN_aesoVdOiTruoebCs896aGxw/edit?tab=t.0" rel="noopener noreferrer"&gt;weekly meeting notes&lt;/a&gt; published.
&lt;/li&gt;
&lt;li&gt;Review the changes to the SLSA standard that are suggested, processed, and accepted via pull requests against &lt;a href="https://github.com/slsa-framework" rel="noopener noreferrer"&gt;https://github.com/slsa-framework&lt;/a&gt;. &lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Buildkite Package Registries: Built-in support for SLSA provenance&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Buildkite complements SLSA and can help accelerate your adoption. Buildkite Package Registries includes native &lt;a href="https://buildkite.com/docs/package-registries/security/slsa-provenance" rel="noopener noreferrer"&gt;provenance support&lt;/a&gt; features that are easy to &lt;a href="https://github.com/buildkite-plugins/generate-provenance-attestation-buildkite-plugin/blob/d9f2ff4d6b745f17cc55b6b91778a0e1a7d45824/examples/envelope.json" rel="noopener noreferrer"&gt;cryptographically sign&lt;/a&gt;, artifact signing (for SLSA Level 1 and 2 support), and dependency management (supporting audit efforts). &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fai34m09ufn6g40eyayqs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fai34m09ufn6g40eyayqs.png" alt="The SLSA provenance of a Python package built with Buildkite Package Registries." width="800" height="830"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The SLSA provenance of a Python package built with Buildkite Package Registries.&lt;/p&gt;

&lt;p&gt;If you're looking to boost your software's security framework, consider exploring how Buildkite can help enhance your SLSA efforts. &lt;a href="https://buildkite.com/docs/package-registries/security/slsa-provenance" rel="noopener noreferrer"&gt;Check out the docs&lt;/a&gt; to learn more and get started. &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Summary&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;SLSA is a security framework designed to protect software-delivery supply chains from vulnerabilities and targeted attacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It has structured and standardized security levels (0→3) for its Build track.
&lt;/li&gt;
&lt;li&gt;Its levels are designed to be adopted incrementally, with each level extending lower levels.
&lt;/li&gt;
&lt;li&gt;Implementing its guidance and complying with the requirements of each level increases the security and stability of your builds.
&lt;/li&gt;
&lt;li&gt;SLSA provides a clear and optimized path toward securing build chains and their dependencies.
&lt;/li&gt;
&lt;li&gt;Buildkite Package Registries offers &lt;a href="https://buildkite.com/docs/package-registries/security/slsa-provenance" rel="noopener noreferrer"&gt;built-in support for SLSA provenance and attestation&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cicd</category>
      <category>packaging</category>
      <category>security</category>
      <category>buildkite</category>
    </item>
    <item>
      <title>How to Create and Share a Pulumi Template</title>
      <dc:creator>Christian Nunciato</dc:creator>
      <pubDate>Tue, 20 Dec 2022 18:23:05 +0000</pubDate>
      <link>https://forem.com/pulumi/how-to-create-and-share-a-pulumi-template-15ld</link>
      <guid>https://forem.com/pulumi/how-to-create-and-share-a-pulumi-template-15ld</guid>
      <description>&lt;p&gt;Last month, we released our first set of &lt;a href="https://www.pulumi.com/templates/" rel="noopener noreferrer"&gt;architecture templates&lt;/a&gt; — configurable Pulumi projects designed to make it easy to bootstrap new stacks for common cloud architectures like &lt;a href="https://www.pulumi.com/templates/static-website/" rel="noopener noreferrer"&gt;static websites&lt;/a&gt;, &lt;a href="https://www.pulumi.com/templates/container-service/" rel="noopener noreferrer"&gt;containers&lt;/a&gt;, &lt;a href="https://www.pulumi.com/templates/virtual-machine/" rel="noopener noreferrer"&gt;virtual machines&lt;/a&gt;, and &lt;a href="https://www.pulumi.com/templates/kubernetes/" rel="noopener noreferrer"&gt;Kubernetes clusters&lt;/a&gt;. Architecture templates are a great way to get a new project up and running quickly, and they've already grown quite popular with our users, several of whom have asked if whether it's possible to create templates of their own.&lt;/p&gt;

&lt;p&gt;It's not only possible, it's surprisingly easy! So in this post, I'll show you how. It all starts with a Pulumi project — a template is really just a Pulumi project with a few extra lines of config — so we'll start with one of those, add a few resources, and turn the project into a template. When it's ready to go, we'll test it out locally, and then finally, we'll publish it so that anyone with Pulumi can use our template to kickstart their own projects.&lt;/p&gt;

&lt;p&gt;We'll do this in five steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new project&lt;/li&gt;
&lt;li&gt;Design and build the program&lt;/li&gt;
&lt;li&gt;Turn the project into a template&lt;/li&gt;
&lt;li&gt;Test-drive the template locally&lt;/li&gt;
&lt;li&gt;
Publish the template to share it with the world&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's get to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create a new project
&lt;/h2&gt;

&lt;p&gt;If you haven't already, make sure you've &lt;a href="https://www.pulumi.com/docs/get-started/install/" rel="noopener noreferrer"&gt;installed Pulumi&lt;/a&gt; and &lt;a href="https://www.pulumi.com/registry/packages/aws/installation-configuration/" rel="noopener noreferrer"&gt;configured your AWS credentials&lt;/a&gt;. For this example, we'll be using &lt;a href="https://www.pulumi.com/registry/packages/aws/installation-configuration/" rel="noopener noreferrer"&gt;AWS&lt;/a&gt;, but the process is the same for any cloud provider, so if you aren't set up on AWS, you should still be able to follow along anyway.&lt;/p&gt;

&lt;p&gt;Create a new project in the usual way using one of our starter templates. The YAML template is a good choice for this walkthrough, as YAML projects are simple and lightweight (no runtime dependencies!), and because they combine both project and program into one file, they're especially conducive to sharing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;my-template-project &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;my-template-project
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi new aws-yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step through the prompts, choosing whichever AWS region works best for you. When the new-project wizard finishes, open the generated &lt;code&gt;Pulumi.yaml&lt;/code&gt; project file in your editor of choice to start building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Design and build the program
&lt;/h2&gt;

&lt;p&gt;I'm a static-website person, myself — I think pretty much everyone should have one. So in order to realize my grand vision of home page for every human, you and I are going to build a Pulumi project template that lets anyone with the Pulumi CLI deploy a super-simple website of their own on AWS.&lt;/p&gt;

&lt;p&gt;As Pulumi template authors, our general goal is twofold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make it easy for our users to create deployable projects that address an infrastructural need.&lt;/li&gt;
&lt;li&gt;Make it easy to customize and extend those projects after they've been created.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good template, in other words, is one that not only takes you from &lt;code&gt;pulumi new&lt;/code&gt; to &lt;code&gt;pulumi up&lt;/code&gt; in as few steps as possible, but that also leaves you with an open, extensible program you can use as a foundation to build upon. For us, the goal is to create a template that can be used to provision the minimal set of cloud resources one needs to run a static website on AWS. And done well, our template will also lend itself easily to further development — custom domains, serverless functions, edge caching, and so on.&lt;/p&gt;

&lt;p&gt;To that end, let's start by replacing the contents of &lt;code&gt;Pulumi.yaml&lt;/code&gt; with the following program, which defines the core resources we need: an &lt;a href="https://www.pulumi.com/registry/packages/aws/api-docs/s3/bucket/" rel="noopener noreferrer"&gt;S3 bucket&lt;/a&gt; to hold the files of the website and an &lt;a href="https://www.pulumi.com/registry/packages/aws/api-docs/s3/bucketobject/" rel="noopener noreferrer"&gt;S3 bucket object&lt;/a&gt; (&lt;code&gt;index.html&lt;/code&gt;) to serve as its home page. We'll also export the computed URL of the website as a Pulumi &lt;a href="https://www.pulumi.com/docs/intro/concepts/stack/#outputs" rel="noopener noreferrer"&gt;stack output&lt;/a&gt; to give us something to navigate to after deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-template-project&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A simple static website on Amazon S3&lt;/span&gt;
&lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yaml&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="c1"&gt;# Create an S3 bucket and configure it as a website.&lt;/span&gt;
  &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:s3:Bucket&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;acl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public-read&lt;/span&gt;
      &lt;span class="na"&gt;website&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;indexDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index.html&lt;/span&gt;

  &lt;span class="c1"&gt;# Create a home page for the website.&lt;/span&gt;
  &lt;span class="na"&gt;index.html&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:s3:BucketObject&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${bucket.id}&lt;/span&gt;
      &lt;span class="na"&gt;acl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public-read&lt;/span&gt;
      &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;text/html&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;&amp;lt;meta name="content" charset="utf-8"&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;&amp;lt;title&amp;gt;My Awesome Website&amp;lt;/title&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
            &lt;span class="s"&gt;&amp;lt;h1&amp;gt;My Awesome Website&amp;lt;/h1&amp;gt;&lt;/span&gt;
            &lt;span class="s"&gt;&amp;lt;p&amp;gt;Hello, internet person! 👋&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
        &lt;span class="s"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;# Export the URL of the website.&lt;/span&gt;
&lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://${bucket.websiteEndpoint}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try running this program now, just to make sure everything works. In a few seconds, you should see that the new resources were created and be able to browse to the new website at the exported &lt;code&gt;url&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pulumi up
...

Updating (dev)

     Type                    Name                        Status
 +   pulumi:pulumi:Stack     my-s3-website-template-dev  created (3s)
 +   ├─ aws:s3:Bucket        bucket                      created (2s)
 +   └─ aws:s3:BucketObject  index.html                  created (0.37s)

Outputs:
    url: "http://bucket-f1bb181.s3-website-us-west-2.amazonaws.com"

Resources:
    + 3 created

Duration: 5s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good — we have a working program. And looking back at our design goals, you'll see that this program already meets them pretty well:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It works out of the box. Deployed now, it produces a running website on AWS.&lt;/li&gt;
&lt;li&gt;It's open and extensible. To build onto it — add more pages, attach a custom domain, etc. — you'd simply add more resources alongside those already defined.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now let's turn this program into a template.&lt;/p&gt;

&lt;p&gt;Before moving on, though, let's quickly destroy and remove this stack, as we'll be making a few changes to the project's settings in the next step, and it'd be better to begin from a clean slate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi destroy &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="nt"&gt;--remove&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Turn the project into a template
&lt;/h2&gt;

&lt;p&gt;The next thing to do is decide which parts of the program should be configurable. Earlier, when you ran &lt;code&gt;pulumi new aws-yaml&lt;/code&gt;, you probably noticed you were prompted for an optional AWS region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pulumi new aws-yaml
...

This command will walk you through creating a new Pulumi project.
...

aws:region: The AWS region to deploy into: (us-west-2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You got this prompt because the authors of the &lt;a href="https://github.com/pulumi/templates/tree/master/aws-yaml" rel="noopener noreferrer"&gt;&lt;code&gt;aws-yaml&lt;/code&gt; template&lt;/a&gt; knew that not every user would want to deploy into the same hard-coded AWS region, so they defined an &lt;code&gt;aws:region&lt;/code&gt; setting to make it both configurable and optional, falling back to &lt;code&gt;us-west-2&lt;/code&gt; by default. Users of this template are free to change this value if they like or leave it alone and accept the default.&lt;/p&gt;

&lt;p&gt;This is all made possible by the existence of the &lt;a href="https://www.pulumi.com/docs/reference/pulumi-yaml/#template-options" rel="noopener noreferrer"&gt;&lt;code&gt;template&lt;/code&gt; block&lt;/a&gt; in &lt;code&gt;Pulumi.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# The `template` block from the aws-yaml template.&lt;/span&gt;
&lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A minimal AWS Pulumi YAML program&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;aws:region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The AWS region to deploy into&lt;/span&gt;
      &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-west-2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Indeed, this block is all that defines &lt;code&gt;aws-yaml&lt;/code&gt; as a template; without it, the &lt;code&gt;aws-yaml&lt;/code&gt; project would be a regular ol' Pulumi YAML program like any other.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;template&lt;/code&gt; block defines two properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An optional &lt;code&gt;description&lt;/code&gt; property to give new projects created from the template&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;config&lt;/code&gt; block that lists the names, descriptions, and default values of any settings that should be configurable for new projects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;template&lt;/code&gt; block is essentially where all the new-project magic happens. Any settings you define in this block will prompt users for their values and apply those settings to the project's initial stack (for example, the &lt;code&gt;dev&lt;/code&gt; stack we created in Step 1). By default, these values are captured and applied as plain-text strings, but you can also capture them &lt;a href="https://www.pulumi.com/docs/reference/pulumi-yaml/#config" rel="noopener noreferrer"&gt;as encrypted Pulumi secrets&lt;/a&gt; by adding &lt;code&gt;secret: true&lt;/code&gt; — an appropriate choice for prompting for sensitive data like passwords, API keys, and the like.&lt;/p&gt;

&lt;p&gt;Our super-simple website template doesn't need much in the way of configurability — but let's define some anyway just to see how it's done. While we're at it, we'll make a few adjustments to make project names and descriptions configurable as well.&lt;/p&gt;

&lt;p&gt;Make the changes below to &lt;code&gt;Pulumi.yaml&lt;/code&gt; to use the project &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; supplied by the CLI, add a &lt;code&gt;template&lt;/code&gt; section to let users configure a home-page title and greeting, and interpolate those values into the body of the home page itself. Here's the full content of the program for reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Configure project names and descriptions with values obtained from `pulumi new`.&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${PROJECT}&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DESCRIPTION}&lt;/span&gt;
&lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yaml&lt;/span&gt;

&lt;span class="c1"&gt;# Define the template's configuration settings.&lt;/span&gt;
&lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A simple static website on Amazon S3&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;aws:region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The AWS region to deploy into&lt;/span&gt;
      &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-west-2&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A title to use for the home page&lt;/span&gt;
      &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;My Awesome Website&lt;/span&gt;
    &lt;span class="na"&gt;greeting&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A greeting to show on the home page&lt;/span&gt;
      &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hello, internet person! 👋&lt;/span&gt;

&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="c1"&gt;# Create an S3 bucket and configure it as a website.&lt;/span&gt;
  &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:s3:Bucket&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;acl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public-read&lt;/span&gt;
      &lt;span class="na"&gt;website&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;indexDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index.html&lt;/span&gt;

  &lt;span class="c1"&gt;# Create a home page for the website.&lt;/span&gt;
  &lt;span class="na"&gt;index.html&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:s3:BucketObject&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${bucket.id}&lt;/span&gt;
      &lt;span class="na"&gt;acl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public-read&lt;/span&gt;
      &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;text/html&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;&amp;lt;meta name="content" charset="utf-8"&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;&amp;lt;title&amp;gt;${title}&amp;lt;/title&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
            &lt;span class="s"&gt;&amp;lt;h1&amp;gt;${title}&amp;lt;/h1&amp;gt;&lt;/span&gt;
            &lt;span class="s"&gt;&amp;lt;p&amp;gt;${greeting}&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
        &lt;span class="s"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;# Export the URL of the website.&lt;/span&gt;
&lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://${bucket.websiteEndpoint}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all there is to it! The template's complete and ready to be published. However, before we do that, let's first give it a try locally just to make sure it delivers the new-project experience we're looking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Test-drive the template locally
&lt;/h2&gt;

&lt;p&gt;Back in your terminal (and assuming you're still in the &lt;code&gt;my-template-project&lt;/code&gt; folder), create a new folder alongside &lt;code&gt;my-template-project&lt;/code&gt;, then run &lt;code&gt;pulumi new&lt;/code&gt; to create a new project, pointing Pulumi to the folder containing your template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ..
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;my-test-website &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;my-test-website
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi new ../my-template-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step through the prompts, which should include each of the configurable settings we defined in the &lt;code&gt;template&lt;/code&gt; block earlier, along with their default values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pulumi new ./my-template-project

project name: (my-test-website)
project description: (A simple static website on Amazon S3)
...

aws:region: The AWS region to deploy into: (us-west-2)
greeting: A greeting to show on the home page: (Hello, internet person! 👋) Hi, world!
title: A title to use for the home page: (My Awesome Website) Hey look, it's my website!

Your new project is ready to go! ✨
To perform an initial deployment, run `pulumi up`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go ahead and deploy the new project with &lt;code&gt;pulumi up&lt;/code&gt;, then navigate to the newly deployed website in your favorite web browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;open &lt;span class="si"&gt;$(&lt;/span&gt;pulumi stack output url&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well done! You've written your first Pulumi template.&lt;/p&gt;

&lt;p&gt;Let's finish things off by publishing your template so others can use it. As before, tidy up with &lt;code&gt;pulumi destroy&lt;/code&gt; before moving on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi destroy &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="nt"&gt;--remove&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Publish the template
&lt;/h2&gt;

&lt;p&gt;A moment ago, when you tested your first template, you did so by pointing Pulumi to the template's path on your local filesystem. Before that, when you ran &lt;code&gt;pulumi new aws-yaml&lt;/code&gt;, you instructed Pulumi (implicitly) to fetch the source of the template over HTTPS from the official &lt;a href="https://github.com/pulumi/templates" rel="noopener noreferrer"&gt;pulumi/templates repository on GitHub&lt;/a&gt;. So to make your template available to others, all you have to do publish it to a file path or Git URL that's accessible by the Pulumi CLI.&lt;/p&gt;

&lt;p&gt;You &lt;em&gt;could&lt;/em&gt; create a new GitHub repository for this project, then pass the fully-qualified URL of the path containing &lt;code&gt;Pulumi.yaml&lt;/code&gt; to &lt;code&gt;pulumi new&lt;/code&gt;, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi new https://github.com/&lt;span class="o"&gt;{&lt;/span&gt;your-org&lt;span class="o"&gt;}&lt;/span&gt;/&lt;span class="o"&gt;{&lt;/span&gt;your-repo&lt;span class="o"&gt;}&lt;/span&gt;/tree/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But since our project consists of just one file, you don't even need to do that much: you can just paste the contents of the file into a new &lt;a href="https://gist.github.com/" rel="noopener noreferrer"&gt;GitHub gist&lt;/a&gt; (since a gist is &lt;a href="https://docs.github.com/en/get-started/writing-on-github/editing-and-sharing-content-with-gists/creating-gists" rel="noopener noreferrer"&gt;also a Git repository&lt;/a&gt;), name the gist &lt;code&gt;Pulumi.yaml&lt;/code&gt;, and point Pulumi to the gist's URL.&lt;/p&gt;

&lt;p&gt;Go ahead and &lt;a href="https://gist.github.com/" rel="noopener noreferrer"&gt;create a gist of your own&lt;/a&gt;, then use it to create a new project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; ../my-second-test-website &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ../my-second-test-website
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi new https://gist.github.com/cnunciato/b331efae6a4740c237a0364d17fe220f
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be sure to tidy up as before with &lt;code&gt;pulumi destroy&lt;/code&gt; when you're done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus step: Add a Deploy with Pulumi button
&lt;/h2&gt;

&lt;p&gt;In addition to the CLI, your users can also create new projects in the Pulumi Service with the &lt;a href="https://www.pulumi.com/docs/intro/pulumi-service/pulumi-button/" rel="noopener noreferrer"&gt;Deploy with Pulumi button&lt;/a&gt;. This is a great option for making your project installable from GitHub READMEs and other team docs. Here, for example, is a Deploy button that creates a new project using my version of the gist we created above:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.pulumi.com/new?template=https://gist.github.com/cnunciato/b331efae6a4740c237a0364d17fe220f" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fget.pulumi.com%2Fnew%2Fbutton.svg" alt="Deploy with Pulumi" width="215" height="46"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Embedding these buttons yourself is easy — just use one of the snippets below, swapping the values of &lt;code&gt;{template-url}&lt;/code&gt; for the URL of your template's gist or Git repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://app.pulumi.com/new?template={template-url}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;https:&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="na"&gt;get.pulumi.com&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;new&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;button.svg&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"Deploy with Pulumi"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;![Deploy with Pulumi&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://get.pulumi.com/new/button.svg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;](https://app.pulumi.com/new?template={template-url})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Project creators who go down this path will be prompted in the browser for the same configuration values as they would with the CLI, and afterward, they'll be able to deploy the project either with the Pulumi CLI or with &lt;a href="https://www.pulumi.com/docs/intro/pulumi-service/deployments/" rel="noopener noreferrer"&gt;Pulumi Deployments&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;As you can see, creating a template is both simple and powerful, and I hope this post encourages you to experiment with a few of your own. Feel free to peruse the following links for inspiration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/templates/" rel="noopener noreferrer"&gt;Pulumi Templates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/pulumi/templates" rel="noopener noreferrer"&gt;pulumi/templates repository&lt;/a&gt; on GitHub&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/docs/reference/pulumi-yaml" rel="noopener noreferrer"&gt;Pulumi project file reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/docs/reference/cli/pulumi_new/" rel="noopener noreferrer"&gt;&lt;code&gt;pulumi new&lt;/code&gt; reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/docs/intro/pulumi-service/pulumi-button/" rel="noopener noreferrer"&gt;Deploy with Pulumi button reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And as always, be sure to stop by &lt;a href="https://slack.pulumi.com/" rel="noopener noreferrer"&gt;Pulumi Community Slack&lt;/a&gt; to let us know know how it goes.&lt;/p&gt;

&lt;p&gt;Happy templating!&lt;/p&gt;

</description>
      <category>watercooler</category>
    </item>
    <item>
      <title>Deploying a Data Warehouse with Pulumi and Amazon Redshift</title>
      <dc:creator>Christian Nunciato</dc:creator>
      <pubDate>Wed, 30 Nov 2022 23:14:36 +0000</pubDate>
      <link>https://forem.com/pulumi/deploying-a-data-warehouse-with-pulumi-and-amazon-redshift-112b</link>
      <guid>https://forem.com/pulumi/deploying-a-data-warehouse-with-pulumi-and-amazon-redshift-112b</guid>
      <description>&lt;p&gt;It's fun to think about how much data there is swirling around in the global datasphere these days. However you choose to measure it (and there are various ways), it's a quantity so massive — &lt;a href="https://en.wikipedia.org/wiki/Zettabyte_Era" rel="noopener noreferrer"&gt;hundreds of zettabytes&lt;/a&gt;, by some estimates — that it's kind of a hard thing to quite get your head around.&lt;/p&gt;

&lt;p&gt;If you could convert all the world's data into droplets of water, for instance, at one megabyte per drop, you'd have enough 1MB drops to fill two more &lt;a href="https://en.wikipedia.org/wiki/Lake_Washington" rel="noopener noreferrer"&gt;Lake Washingtons&lt;/a&gt;. If you could store all that data on &lt;a href="https://en.wikipedia.org/wiki/Floppy_disk#3%C2%BD-inch_disk" rel="noopener noreferrer"&gt;3.5" floppies&lt;/a&gt;, you'd need more than a hundred quadrillion floppies to capture it all — enough to cover the planet entirely (with much room for overlap) or to pave a nice bridge for yourself from your front porch well into interstellar space. If you could pull all that data into an HD movie, and you sat down to start watching that movie 2.5 million years ago (with your favorite saber-toothed friend, say), you'd still be watching the same movie today.&lt;/p&gt;

&lt;p&gt;That's a lot of data — and there's more every day.&lt;/p&gt;

&lt;p&gt;Of course, the volume of data &lt;em&gt;you&lt;/em&gt; care about is probably a lot more modest. But even so, there's a good chance whatever system you're building can and will produce useful data of its own, and there's an equally good chance you'll want to learn something from it. That data will likely exist in many forms and in multiple places — flat file here, transactional database there, the REST API of some third-party service — so before you can really &lt;em&gt;look&lt;/em&gt; at it, you'll need to figure out how to get it all into one place. And for that, you might want to consider a &lt;a href="https://en.wikipedia.org/wiki/Data_warehouse" rel="noopener noreferrer"&gt;data warehouse&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A data warehouse is a specialized database that's purpose built for gathering and analyzing data. Unlike general-purpose databases like MySQL or PostgreSQL, which are designed to meet the real-time performance and transactional needs of applications, a data warehouse is designed to collect and process the data produced by those applications, collectively and over time, to help you gain insight from it. Examples of data-warehouse products include &lt;a href="https://www.snowflake.com/en/data-cloud/workloads/data-warehouse/" rel="noopener noreferrer"&gt;Snowflake&lt;/a&gt;, &lt;a href="https://cloud.google.com/bigquery/" rel="noopener noreferrer"&gt;Google BigQuery&lt;/a&gt;, &lt;a href="https://azure.microsoft.com/en-us/products/synapse-analytics" rel="noopener noreferrer"&gt;Azure Synapse Analytics&lt;/a&gt;, and &lt;a href="https://aws.amazon.com/redshift/" rel="noopener noreferrer"&gt;Amazon Redshift&lt;/a&gt; — all of which, incidentally, are easily managed with Pulumi.&lt;/p&gt;

&lt;p&gt;Today, though, we're going to focus on Amazon Redshift. Specifically, we're going to walk through the process of writing a Pulumi program that provisions a single-node Redshift &lt;a href="https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-clusters.html" rel="noopener noreferrer"&gt;cluster&lt;/a&gt; in an &lt;a href="https://aws.amazon.com/vpc/" rel="noopener noreferrer"&gt;Amazon VPC&lt;/a&gt;, then we'll load some sample data into the warehouse from Amazon S3. We'll load this data manually at first, just to get a sense of how everything works when it's all wired up, and then later, in a follow-up post, we'll go a step further and weave in some automation to load the data on a schedule.&lt;/p&gt;

&lt;p&gt;Let's get going!&lt;/p&gt;

&lt;p&gt;We'll begin, as always, by creating a new Pulumi project. (And if you haven't already, make sure you've &lt;a href="https://www.pulumi.com/docs/get-started/install/" rel="noopener noreferrer"&gt;installed Pulumi&lt;/a&gt; and &lt;a href="https://www.pulumi.com/registry/packages/aws/installation-configuration/" rel="noopener noreferrer"&gt;configured your AWS credentials&lt;/a&gt; in the usual way.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;my-data-warehouse &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;my-data-warehouse
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi new aws-python
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step through the new-project wizard, accepting the defaults if they'll work for you. For this project, we'll deploy both cluster and data into the &lt;code&gt;us-west-2&lt;/code&gt; region of AWS, but you can change the target to region you like, as both S3 and Redshift are supported in all AWS regions.&lt;/p&gt;

&lt;p&gt;With the project and stack in place, it's time to start building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure the stack
&lt;/h2&gt;

&lt;p&gt;To launch a new Redshift cluster, you'll need to give AWS a few details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A unique (in the scope of your AWS account) name for the Redshift cluster&lt;/li&gt;
&lt;li&gt;A name for the cluster's initial database&lt;/li&gt;
&lt;li&gt;A username and password for the cluster's administrative user&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-clusters.html#rs-node-type-info" rel="noopener noreferrer"&gt;node type&lt;/a&gt; to use for the Redshift cluster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The following configuration settings should work for this walkthrough (but again, feel free to adjust them if you'd like):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;clusterIdentifier my-redshift-cluster
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;clusterNodeType ra3.xlplus
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;clusterDBName dev
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;clusterDBUsername admin
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;clusterDBPassword StrongPass1 &lt;span class="nt"&gt;--secret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Compose the program
&lt;/h2&gt;

&lt;p&gt;With these settings in place, you can start writing the Pulumi program.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;__main__.py&lt;/code&gt; in your editor of choice and replace its contents with the following code, which imports the Pulumi and AWS libraries, reads the configuration values you just set, and provisions an S3 bucket that we'll use to store some raw, unstructured data later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pulumi&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pulumi_aws&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redshift&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;

&lt;span class="c1"&gt;# Import the stack's configuration settings.
&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;cluster_identifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clusterIdentifier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cluster_node_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clusterNodeType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cluster_db_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clusterDBName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cluster_db_username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clusterDBUsername&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cluster_db_password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clusterDBPassword&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Import the provider's configuration settings.
&lt;/span&gt;&lt;span class="n"&gt;provider_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;aws_region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Create an S3 bucket to store some raw data.
&lt;/span&gt;&lt;span class="n"&gt;events_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BucketArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;force_destroy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, define a new VPC and the associated network resources for the Redshift cluster. Since the aim is to launch the cluster into a VPC (providing the network isolation we need to keep the cluster from being accessed over the internet), you'll define the VPC first, then define a private subnet within it, and then finally designate a &lt;a href="https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-cluster-subnet-groups.html" rel="noopener noreferrer"&gt;Redshift subnet group&lt;/a&gt; to allow to tell AWS where to provision the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ...
&lt;/span&gt;
&lt;span class="c1"&gt;# Create a VPC.
&lt;/span&gt;&lt;span class="n"&gt;vpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Vpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vpc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VpcArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;cidr_block&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.0.0/16&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;enable_dns_hostnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Create a private subnet within the VPC.
&lt;/span&gt;&lt;span class="n"&gt;subnet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Subnet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subnet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SubnetArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;vpc_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cidr_block&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0.1.0/24&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Declare a Redshift subnet group with the subnet ID.
&lt;/span&gt;&lt;span class="n"&gt;subnet_group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redshift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SubnetGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subnet-group&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redshift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SubnetGroupArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;subnet_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, on to the cluster itself.&lt;/p&gt;

&lt;p&gt;The plan, you'll recall, is to pull data from an S3 bucket into Redshift (using the Redshift service itself, for now), so you'll need to give Redshift the appropriate permissions to read from Amazon S3. You can do that with an IAM role and &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/security-iam-awsmanpol.html#security-iam-awsmanpol-amazons3readonlyaccess" rel="noopener noreferrer"&gt;AWS-managed policy&lt;/a&gt; granting Redshift read-only access to the S3 service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ...
&lt;/span&gt;
&lt;span class="c1"&gt;# Create an IAM role granting Redshift read-only access to S3.
&lt;/span&gt;&lt;span class="n"&gt;redshift_role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redshift-role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RoleArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;assume_role_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2012-10-17&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Statement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sts:AssumeRole&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Effect&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Allow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Principal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redshift.amazonaws.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="n"&gt;managed_policy_arns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ManagedPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AMAZON_S3_READ_ONLY_ACCESS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That takes care of the permissions part — but there's one last thing you'll need to do to make this work. Because the cluster will reside in a private subnet, and that subnet won't have access to the public internet, you'll need to give the cluster a way to communicate with S3 without having to leave the VPC network. You can do this by adding a &lt;a href="https://docs.aws.amazon.com/vpc/latest/privatelink/vpc-endpoints-s3.html" rel="noopener noreferrer"&gt;VPC gateway endpoint&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ...
&lt;/span&gt;
&lt;span class="c1"&gt;# Create a VPC endpoint so the cluster can read from S3 over the private network.
&lt;/span&gt;&lt;span class="n"&gt;vpc_endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VpcEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3-vpc-endpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VpcEndpointArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;vpc_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;service_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;com.amazonaws.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;aws_region&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, add the cluster itself, using everything you've defined up to now — config settings, network settings, IAM role, and the VPC's default security group, which enables traffic to and from the cluster on all ports and subnet IPs. Finish things off by exporting the name of the S3 bucket and Redshift ARN, as you'll need both in order to post and then import the S3 data (both will be &lt;a href="https://dev.to/docs/intro/concepts/resources/names/#autonaming"&gt;auto-named&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ...
&lt;/span&gt;
&lt;span class="c1"&gt;# Create a single-node Redshift cluster in the VPC.
&lt;/span&gt;&lt;span class="n"&gt;cluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redshift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Cluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cluster&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redshift&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClusterArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;cluster_identifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cluster_identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;database_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cluster_db_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;master_username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cluster_db_username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;master_password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cluster_db_password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;node_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cluster_node_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cluster_subnet_group_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subnet_group&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 class="n"&gt;cluster_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single-node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;publicly_accessible&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;skip_final_snapshot&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vpc_security_group_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_security_group_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;iam_roles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;redshift_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Export the bucket name and role ARN.
&lt;/span&gt;&lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataBucketName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redshiftRoleArn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redshift_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with that, you're ready to deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy the stack
&lt;/h2&gt;

&lt;p&gt;Back at the command line, deploy the stack in the usual way with &lt;code&gt;pulumi up&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up
...
Updating &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;

     Type                         Name                   Status
 +   pulumi:pulumi:Stack          my-data-warehouse-dev  created &lt;span class="o"&gt;(&lt;/span&gt;3s&lt;span class="o"&gt;)&lt;/span&gt;
 +   ├─ aws:s3:Bucket             events                 created &lt;span class="o"&gt;(&lt;/span&gt;2s&lt;span class="o"&gt;)&lt;/span&gt;
 +   ├─ aws:ec2:Vpc               vpc                    created &lt;span class="o"&gt;(&lt;/span&gt;12s&lt;span class="o"&gt;)&lt;/span&gt;
 +   ├─ aws:iam:Role              redshift-role          created &lt;span class="o"&gt;(&lt;/span&gt;1s&lt;span class="o"&gt;)&lt;/span&gt;
 +   ├─ aws:ec2:Subnet            subnet                 created &lt;span class="o"&gt;(&lt;/span&gt;0.81s&lt;span class="o"&gt;)&lt;/span&gt;
 +   ├─ aws:ec2:VpcEndpoint       s3-vpc-endpoint        created &lt;span class="o"&gt;(&lt;/span&gt;6s&lt;span class="o"&gt;)&lt;/span&gt;
 +   ├─ aws:redshift:SubnetGroup  subnet-group           created &lt;span class="o"&gt;(&lt;/span&gt;0.80s&lt;span class="o"&gt;)&lt;/span&gt;
 +   └─ aws:redshift:Cluster      cluster                created &lt;span class="o"&gt;(&lt;/span&gt;270s&lt;span class="o"&gt;)&lt;/span&gt;

Outputs:
    dataBucketName : &lt;span class="s2"&gt;"events-50d8c96"&lt;/span&gt;
    redshiftRoleArn: &lt;span class="s2"&gt;"arn:aws:iam::616138583583:role/redshift-role-47ad8f0"&lt;/span&gt;

Resources:
    + 8 created

Duration: 4m48s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a few moments (deploying a new cluster takes about five minutes), you should see that the new cluster and all of its resources were created. Again, you won't be able to access the cluster over the internet, as it'll have been deployed into an isolated VPC network, but you should be able to browse to the cluster in the AWS Console and start running some queries with it.&lt;/p&gt;

&lt;p&gt;Choose &lt;strong&gt;Query data &amp;gt; Query in query editor v2&lt;/strong&gt; to open the Redshift query editor:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fepcanqahlhe3619fxrbx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fepcanqahlhe3619fxrbx.png" alt="The newly created cluster in the AWS Console" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then, from the query editor, find your newly created cluster in the left-hand pane and connect using the database, username, and password you configured earlier:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz2y2fo3vmhjezdibvhtx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz2y2fo3vmhjezdibvhtx.png" alt="The connection dialog for the newly created cluster" width="800" height="669"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Choose &lt;strong&gt;Create connection&lt;/strong&gt;, expand &lt;code&gt;my-redshift-cluster&lt;/code&gt;, and you should see the &lt;code&gt;dev&lt;/code&gt; database there, ready to go (only without any tables — we'll get to that next):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk0u5ajdhoz6b62pyo3l8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk0u5ajdhoz6b62pyo3l8.png" alt="The Redshift query editor, now connected to the cluster and database" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now let's load some data!&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting data into Redshift
&lt;/h2&gt;

&lt;p&gt;In a real-world situation, you'd probably already have some data to load — web server logs, for example, or some other sort of raw or unstructured data residing in an S3 bucket, a DynamoDB table, an RDS database, etc. But we're starting from scratch, so we'll create and publish that data manually.&lt;/p&gt;

&lt;p&gt;Copy and paste the following command to generate a text file containing a few lines of JSON, each representing a fictitious event to be loaded into the data warehouse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"id": 1, "name": "An interesting event"}
{"id": 2, "name": "Another interesting event"}
{"id": 3, "name": "An event of monumental importance"}'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; events-1.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice there aren't any commas between these events, so it's not actually a valid JSON &lt;em&gt;file&lt;/em&gt; -- it's just a text file containing some individually valid JSON events, one event per line. Adhering to this format allows Redshift to ingest and map the data to table we create next &lt;a href="https://docs.aws.amazon.com/redshift/latest/dg/copy-parameters-data-format.html#copy-json-jsonpaths" rel="noopener noreferrer"&gt;using one of its built-in parsers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Upload the file to S3 with the AWS CLI, using Pulumi to supply the name of the destination bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;events-1.txt &lt;span class="s2"&gt;"s3://&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pulumi stack output dataBucketName&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, you have all you need in terms of infrastructure: You've got a single-node Redshift cluster, an empty Redshift database, some unstructured S3 data, and the right set of permissions and network settings to allow the cluster to reach and import the data. All that's left is to run a couple of SQL queries to make that happen: one to create a new table to hold the data, another to load it.&lt;/p&gt;

&lt;p&gt;Back in the Redshift query editor, create the new table by choosing the &lt;code&gt;dev&lt;/code&gt; database, pasting the following query into the editor, and hitting &lt;strong&gt;Run&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see that the query succeeded and that the table was created:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe7gz23a5r56oqvz7k2da.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe7gz23a5r56oqvz7k2da.png" alt="The Redshift query editor showing the events table was created successfully" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you're ready to import the data. Replace the contents of the query editor with the following &lt;a href="https://docs.aws.amazon.com/redshift/latest/dg/r_COPY.html" rel="noopener noreferrer"&gt;Redshift &lt;code&gt;COPY command&lt;/code&gt;&lt;/a&gt;, replacing the &lt;code&gt;{bucket-name}&lt;/code&gt;, &lt;code&gt;{iam-role}&lt;/code&gt;, and &lt;code&gt;{aws-region}&lt;/code&gt; placeholders with the corresponding values from your Pulumi stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="s1"&gt;'s3://{bucket-name}/events-1.txt'&lt;/span&gt;
&lt;span class="n"&gt;IAM_ROLE&lt;/span&gt; &lt;span class="s1"&gt;'{iam-role}'&lt;/span&gt;
&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt; &lt;span class="s1"&gt;'auto'&lt;/span&gt;
&lt;span class="n"&gt;REGION&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="s1"&gt;'{aws-region}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what I see, for example, after running that command:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzlgcexmgdyytc61rfjwm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzlgcexmgdyytc61rfjwm.png" alt="The Redshift query editor showing the data was imported successfully" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the &lt;code&gt;COPY&lt;/code&gt; command completes (which may take a few seconds), you should be able to run a new query the &lt;code&gt;events&lt;/code&gt; table and see the new data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5xm5112uq17qb6vtf1w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo5xm5112uq17qb6vtf1w.png" alt="The Redshift query editor showing three new records retrieved from the dev database" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Huzzah! You've now got yourself a working data warehouse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taking stock, and looking ahead
&lt;/h2&gt;

&lt;p&gt;If you're like me, you're probably happy to be up and running — but you've also got a few questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;What would happen if you added a few &lt;em&gt;more&lt;/em&gt; rows to &lt;code&gt;events-1.txt&lt;/code&gt;, then ran that &lt;code&gt;COPY&lt;/code&gt; command a second time? Would Redshift ignore the records it's already processed, and only import the new ones?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Running SQL commands in a browser like this seems less than ideal. Can I run this process automatically somehow — like once a day, or in response to some other event?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;My source data's kind of a mess. Can I process the data somehow when I load it — like drop certain fields, rename columns, split records into multiple tables, and so on?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Can I pull data from other sources that &lt;em&gt;aren't&lt;/em&gt; Amazon S3?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All good questions, and I'm glad that you asked them.&lt;/p&gt;

&lt;p&gt;First, as you'll notice, adding lines to &lt;code&gt;events.txt&lt;/code&gt; and running &lt;code&gt;COPY&lt;/code&gt; a second time probably doesn't behave as you might hope:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fep8wyixcpx9gfwod8bzl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fep8wyixcpx9gfwod8bzl.png" alt="The Redshift query editor showing three new duplicate records and one new one" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Duplicate records — the bane of the data scientist.&lt;/p&gt;

&lt;p&gt;As of now, your only real recourse is to leave &lt;code&gt;events-1.txt&lt;/code&gt; alone, create new data files for each batch of events, and only &lt;code&gt;COPY&lt;/code&gt; those files that haven't been processed already. For small projects that don't generate much data, this &lt;em&gt;might&lt;/em&gt; be a viable option, but over time, it'd probably become pretty tedious, and it'd be all too easy to make a mistake that leaves you with messed-up data. Not ideal.&lt;/p&gt;

&lt;p&gt;Instead, what you'll ultimately want is some process by which your data can be &lt;em&gt;extracted&lt;/em&gt; (duplicate-free and from multiple sources), &lt;em&gt;transformed&lt;/em&gt; according to rules you specify (keeping what you want, dropping what you don't), and &lt;em&gt;loaded&lt;/em&gt; into the warehouse without your involvement. What you'll want, in other words, is an &lt;a href="https://en.wikipedia.org/wiki/Extract,_transform,_load" rel="noopener noreferrer"&gt;ETL pipeline&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The good news is that with the data warehouse up and running, you're well on your way already. The bad news is that Redshift alone won't finish the job for you. Redshift is just the destination — the target of your ETL pipeline-to-be. The pipeline itself still needs to be written.&lt;/p&gt;

&lt;p&gt;So in the &lt;em&gt;next&lt;/em&gt; post, we'll do that: We'll take what we've done here, add a few more components with Pulumi and &lt;a href="https://aws.amazon.com/glue/" rel="noopener noreferrer"&gt;AWS Glue&lt;/a&gt;, and wire it all up with a few magical lines of Python scripting.&lt;/p&gt;

&lt;p&gt;Until then, happy coding — and stay tuned!&lt;/p&gt;

</description>
      <category>pulumi</category>
      <category>aws</category>
      <category>cloud</category>
      <category>datascience</category>
    </item>
    <item>
      <title>Shared configuration stacks with AWS Systems Manager</title>
      <dc:creator>Christian Nunciato</dc:creator>
      <pubDate>Fri, 01 Jul 2022 01:58:05 +0000</pubDate>
      <link>https://forem.com/pulumi/shared-configuration-stacks-with-aws-systems-manager-4ooj</link>
      <guid>https://forem.com/pulumi/shared-configuration-stacks-with-aws-systems-manager-4ooj</guid>
      <description>&lt;p&gt;One thing I love about Pulumi is how easy it is to configure a stack. As a builder mainly of web applications, I'm always thinking about how I'll configure my apps from one environment to the next, and being able to use Pulumi's built-in support for &lt;a href="https://www.pulumi.com/docs/intro/concepts/config" rel="noopener noreferrer"&gt;configuration&lt;/a&gt; and &lt;a href="https://www.pulumi.com/docs/intro/concepts/secrets" rel="noopener noreferrer"&gt;secrets&lt;/a&gt; to manage the API keys and database credentials for my dev, staging, and production stacks individually is incredibly convenient.&lt;/p&gt;

&lt;p&gt;For larger teams and organizations, though, where multiple applications rely on a set of common configuration settings --- dozens of apps, say, depending on the the same API service or database --- having to keep all of those config settings in sync across all of those individually can become a bit of a pain. When this happens, you may find yourself looking for ways to extract those settings into some sort of a service to allow you to manage them easily in one place, and in a way that allows any application to inherit them automatically.&lt;/p&gt;

&lt;p&gt;There are lots of ways of addressing this sort of problem. One that I like is &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/what-is-systems-manager.html" rel="noopener noreferrer"&gt;AWS Systems Manager (SSM)&lt;/a&gt; --- specifically, SSM &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html" rel="noopener noreferrer"&gt;Parameter Store&lt;/a&gt;. With Systems Manager and Parameter Store, you can store plain-text and encrypted secrets and expose them easily to any other resource in your AWS infrastructure. Parameter Store is a powerful tool to have in your cloud-programming toolbox --- and combined with Pulumi, and in particular Pulumi &lt;a href="https://www.pulumi.com/docs/intro/concepts/stack#stackreferences" rel="noopener noreferrer"&gt;stack references&lt;/a&gt;, you can use it to build specialized stacks make managing shared configuration simple.&lt;/p&gt;

&lt;p&gt;In this post, I'll show you one way to do this. We'll start with a simple Pulumi stack that defines a few config settings with Parameter Store, then stand up a couple of other stacks to illustrate how to inherit and use those settings. We'll also use &lt;a href="https://www.pulumi.com/blog/pulumi-yaml" rel="noopener noreferrer"&gt;Pulumi YAML&lt;/a&gt;, one of the newest additions to the Pulumi language family, to give you an opportunity to kick the tires on that as well.&lt;/p&gt;

&lt;p&gt;Let's get going.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sketching it out
&lt;/h2&gt;

&lt;p&gt;The plan is to begin with a Pulumi stack to act as the keeper of our hypothetical organization's shared configuration. This stack will define two config settings: a message of the day, which we'll store in plain text, and a &lt;em&gt;secret&lt;/em&gt; message of the day (ssh!), which we'll store as an encrypted secret. Later, we'll build two stacks to act as downstream consumers of this shared-config stack --- one, a static website that'll use our config values to render some text in the browser, the other a serverless function that'll return those values as JSON. Both will use Pulumi &lt;a href="https://www.pulumi.com/docs/intro/concepts/stack#stackreferences" rel="noopener noreferrer"&gt;stack references&lt;/a&gt; to obtain their config values from Parameter Store. Here's a visual:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnz2kneirul66x0vihfg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnnz2kneirul66x0vihfg.png" alt="A diagram of three stacks: shared configuration, website, and serverless API." width="650" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's start Stack 1 --- the &lt;code&gt;shared-config&lt;/code&gt; stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the shared config stack
&lt;/h2&gt;

&lt;p&gt;First, make sure you've &lt;a href="https://www.pulumi.com/docs/get-started/install" rel="noopener noreferrer"&gt;installed&lt;/a&gt; and &lt;a href="https://www.pulumi.com/registry/packages/aws/installation-configuration" rel="noopener noreferrer"&gt;configured Pulumi for AWS&lt;/a&gt;, and then, in your shell of choice, create a new folder to house our three stacks-to-be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;shared-config-with-ssm &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;shared-config-with-ssm
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;shared-config my-website my-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This'll leave you with three folders to work with, one for each stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;tree &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="nb"&gt;.&lt;/span&gt;
├── my-service
├── my-website
└── shared-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change to the &lt;code&gt;shared-config&lt;/code&gt; folder and generate a new &lt;a href="https://www.pulumi.com/docs/intro/languages/yaml" rel="noopener noreferrer"&gt;Pulumi YAML&lt;/a&gt; project, accepting the defaults to create a new &lt;code&gt;dev&lt;/code&gt; stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;shared-config
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi new aws-yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the new-project wizard completes, add two new settings for the &lt;code&gt;dev&lt;/code&gt; stack with &lt;a href="https://www.pulumi.com/docs/reference/cli/pulumi_config_set" rel="noopener noreferrer"&gt;&lt;code&gt;pulumi config set&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;motd &lt;span class="s1"&gt;'Hello from Pulumi!'&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;motd_secret &lt;span class="s1"&gt;'Ssh! This is a secret.'&lt;/span&gt; &lt;span class="nt"&gt;--secret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the two values we're going to make available to Stacks 2 and 3.&lt;/p&gt;

&lt;p&gt;Now let's build out the Pulumi program to provision these items as parameters in AWS Systems Manager. Replace the contents of &lt;code&gt;Pulumi.yaml&lt;/code&gt; with the following YAML program, which reads the config values we just set, declares two new &lt;code&gt;aws.ssm.Parameter&lt;/code&gt; resources (using &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-hierarchies.html" rel="noopener noreferrer"&gt;fully-qualified paths&lt;/a&gt; as a best practice), and exports their parameter names as Pulumi stack outputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared-config&lt;/span&gt;
&lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yaml&lt;/span&gt;

&lt;span class="c1"&gt;# The values we're reading from stack configuration.&lt;/span&gt;
&lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;motd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
  &lt;span class="na"&gt;motd_secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;

&lt;span class="c1"&gt;# The AWS SSM Parameters managed by this stack.&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;motd_param&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:ssm:Parameter&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/shared-config/dev/motd&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${motd}&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
  &lt;span class="na"&gt;motd_secret_param&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:ssm:Parameter&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/shared-config/dev/motd_secret&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${motd_secret}&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SecureString&lt;/span&gt;

&lt;span class="c1"&gt;# Outputs are the fully-qualified AWS SSM Parameter names&lt;/span&gt;
&lt;span class="c1"&gt;# that consuming stacks can use to retrieve associated values.&lt;/span&gt;
&lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;motd_param_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${motd_param.name}&lt;/span&gt;
  &lt;span class="na"&gt;motd_secret_param_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${motd_secret_param.name}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, you might be wondering why we're only exporting our parameters' &lt;em&gt;names&lt;/em&gt;, and not their values.&lt;/p&gt;

&lt;p&gt;We certainly &lt;em&gt;could&lt;/em&gt; export their values, since we already have them at hand. But if we did, there wouldn't be much point to using Parameter Store at all, since Pulumi itself is perfectly capable of managing plain-text and encrypted values, and stack references give other stacks an easy way of accessing them programmatically. Why not export the values, then?&lt;/p&gt;

&lt;p&gt;For one, it'd be less flexible. If we exposed these values as stack outputs, downstream stacks referencing them would only be able to read them at deploy-time --- i.e., &lt;em&gt;during&lt;/em&gt; their own Pulumi updates. For &lt;code&gt;shared-config&lt;/code&gt; values that change rarely, that might be okay, but for others, it'd be less than ideal in that all downstream stacks would have to be updated immediately whenever one of those values were changed.&lt;/p&gt;

&lt;p&gt;Secondly, it keeps secrets a bit more secret. Pulumi is great at protecting the secrets it manages, even &lt;a href="https://www.pulumi.com/docs/intro/concepts/secrets#how-secrets-relate-to-outputs" rel="noopener noreferrer"&gt;across stack boundaries&lt;/a&gt; like these, but applications still need a way to get access to the underlying plain-text values of secrets at runtime. Lambda functions, for example, need access tokens and database passwords to do the work we need them to do --- but figuring out how to provide them with those raw values safely can be tricky. By requiring client applications to fetch secrets from Parameter Store, we can keep their plain-text values out of the bodies of our Lambda functions and prevent them from surfacing inadvertently in the AWS Lambda Console.&lt;/p&gt;

&lt;p&gt;Finally, it leaves room for the occasional out-of-band change. Now and then, you might be faced with having to update a value in Parameter Store directly --- e.g., with the AWS Console or CLI. Downstream stacks not reading from Parameter Store, however, wouldn't have access to these updated values until &lt;em&gt;after&lt;/em&gt; the &lt;code&gt;shared-config&lt;/code&gt; stack were refreshed and redeployed --- and would themselves have to be redeployed in order to use them.&lt;/p&gt;

&lt;p&gt;Perhaps that was more explanation than necessary for what amounts to just a few lines of YAML, but hopefully it gives you a sense of the kinds of things to be thinking about as you explore solutions like this on your own. For now, let's forge ahead and get this first stack deployed.&lt;/p&gt;

&lt;p&gt;Back in your terminal, do that with &lt;code&gt;pulumi up&lt;/code&gt;. The deployment should take only a few seconds to complete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up
...

Updating &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;

     Type                  Name               Status
 +   pulumi:pulumi:Stack   shared-config-dev  created
 +   ├─ aws:ssm:Parameter  motd_param         created
 +   └─ aws:ssm:Parameter  motd_secret_param  created

Outputs:
    motd_param_name       : &lt;span class="s2"&gt;"/shared-config/dev/motd"&lt;/span&gt;
    motd_secret_param_name: &lt;span class="s2"&gt;"/shared-config/dev/motd_secret"&lt;/span&gt;

Resources:
    + 3 created

Duration: 4s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice we've named our outputs clearly, and that the secret message is safely encrypted in &lt;code&gt;Pulumi.dev.yaml&lt;/code&gt; and in Parameter Store. You can confirm the latter by visiting the AWS Console:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F93drc10lu44ta8hmofcu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F93drc10lu44ta8hmofcu.png" alt="The secret message tracked as a secret in the AWS Systems Manager Console" width="400" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now let's put these two new values to practical use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetching configuration at deploy-time
&lt;/h2&gt;

&lt;p&gt;Assuming you're still in the &lt;code&gt;shared-config&lt;/code&gt; folder, change to the &lt;code&gt;my-website&lt;/code&gt; folder you created a few moments ago and generate another YAML project, this one for our humble little website:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../my-website
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi new aws-yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once again, follow the prompts to create a new &lt;code&gt;dev&lt;/code&gt; stack, then replace the contents of &lt;code&gt;Pulumi.yaml&lt;/code&gt; with the program below. Here, we're fetching the names of the parameters being managed by &lt;code&gt;shared-config&lt;/code&gt;, pulling their values from Systems Manager, and creating a one-page website to render those values in the browser. (Be sure to adjust the &lt;a href="https://github.com/pulumi/pulumi-yaml#fnstackreference" rel="noopener noreferrer"&gt;&lt;code&gt;Fn::StackReference&lt;/code&gt;&lt;/a&gt;s to point to your stack instead of mine.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-website&lt;/span&gt;
&lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yaml&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="c1"&gt;# Get the names of the parameters we care about from the shared-config stack.&lt;/span&gt;
  &lt;span class="na"&gt;motd_param_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Fn::StackReference&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cnunciato/shared-config/dev&lt;/span&gt;  &lt;span class="c1"&gt;# &amp;lt;-- Change this.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;motd_param_name&lt;/span&gt;
  &lt;span class="na"&gt;motd_secret_param_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Fn::StackReference&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cnunciato/shared-config/dev&lt;/span&gt;  &lt;span class="c1"&gt;# &amp;lt;-- And this.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;motd_secret_param_name&lt;/span&gt;

  &lt;span class="c1"&gt;# Fetch (and decrypt) their values from Systems Manager.&lt;/span&gt;
  &lt;span class="na"&gt;motd_param&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Fn::Invoke&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:ssm:getParameter&lt;/span&gt;
      &lt;span class="na"&gt;Arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${motd_param_ref}&lt;/span&gt;
  &lt;span class="na"&gt;motd_secret_param&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Fn::Invoke&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:ssm:getParameter&lt;/span&gt;
      &lt;span class="na"&gt;Arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${motd_secret_param_ref}&lt;/span&gt;
        &lt;span class="na"&gt;withDecryption&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="c1"&gt;# Create an S3 bucket and configure it as a website.&lt;/span&gt;
  &lt;span class="na"&gt;my-bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:s3:Bucket&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;website&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;indexDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index.html&lt;/span&gt;

  &lt;span class="c1"&gt;# Create a homepage and render the values.&lt;/span&gt;
  &lt;span class="na"&gt;index.html&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws:s3:BucketObject&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${my-bucket}&lt;/span&gt;
      &lt;span class="na"&gt;acl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public-read&lt;/span&gt;
      &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;text/html&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
            &lt;span class="s"&gt;&amp;lt;meta charset="utf-8"&amp;gt;&lt;/span&gt;
            &lt;span class="s"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
              &lt;span class="s"&gt;span { display: none; }&lt;/span&gt;
              &lt;span class="s"&gt;button:active + span { display: inline; }&lt;/span&gt;
            &lt;span class="s"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
            &lt;span class="s"&gt;&amp;lt;h1&amp;gt;${motd_param.value}&amp;lt;/h1&amp;gt;&lt;/span&gt;
            &lt;span class="s"&gt;&amp;lt;button&amp;gt;Reveal the secret message!&amp;lt;/button&amp;gt;&lt;/span&gt;
            &lt;span class="s"&gt;&amp;lt;span&amp;gt;${motd_secret_param.value}&amp;lt;/span&amp;gt;&lt;/span&gt;
          &lt;span class="s"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
        &lt;span class="s"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;

&lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="c1"&gt;# Export the publicly accessible URL of the website.&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://${my-bucket.websiteEndpoint}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the file and deploy the stack, which again should take only a few seconds to complete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up
...

Updating &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;

     Type                    Name            Status
 +   pulumi:pulumi:Stack     my-website-dev  created
 +   ├─ aws:s3:Bucket        my-bucket       created
 +   └─ aws:s3:BucketObject  index.html      created

Outputs:
    url: &lt;span class="s2"&gt;"http://my-bucket-269e711.s3-website-us-west-2.amazonaws.com"&lt;/span&gt;

Resources:
    + 3 created

Duration: 5s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the exported URL, open the website in your favorite browser, and you should also see the message of the day on the homepage, along with a button you can click to reveal the secret message:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7p8i2nxaodmpi80aymun.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7p8i2nxaodmpi80aymun.png" alt="A website showing the message of the day and secret message." width="464" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're using the Pulumi Service, which tracks dependencies between stacks, you should see that the &lt;code&gt;my-website&lt;/code&gt; stack is now listed as a downstream consumer of &lt;code&gt;shared-config&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fztubz33eep46j19g993h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fztubz33eep46j19g993h.png" alt="The shared-config stack showing my-website as a downstream reference" width="608" height="845"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, when the hypothetical team managing the &lt;code&gt;shared-config&lt;/code&gt; stack decides it's time to update the message of the day, it can do so with a &lt;code&gt;pulumi config set&lt;/code&gt; and &lt;code&gt;pulumi up&lt;/code&gt;, and &lt;code&gt;my-website&lt;/code&gt; will be able to pick up the new message automatically.&lt;/p&gt;

&lt;p&gt;However, it's worth mentioning that that won't happen &lt;em&gt;immediately&lt;/em&gt;, as the &lt;code&gt;my-website&lt;/code&gt; stack is configured to pull the message from Systems Manager at &lt;em&gt;deploy-time&lt;/em&gt;. If we want to reflect the new value on the homepage, we'll either need to deploy the &lt;code&gt;my-website&lt;/code&gt; again or come up with a way to fetch the message at &lt;em&gt;runtime&lt;/em&gt; --- which brings us to our third and final stack, in which you'll learn how to do just that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetching configuration at runtime
&lt;/h2&gt;

&lt;p&gt;The easiest way to see this in action is with &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" rel="noopener noreferrer"&gt;AWS Lambda&lt;/a&gt;. With Lambda and Pulumi's support for &lt;a href="https://www.pulumi.com/docs/intro/concepts/function-serialization" rel="noopener noreferrer"&gt;function serialization&lt;/a&gt;, we can capture the parameter names with stack references, pass them into the Lambda at deploy-time (as plain-text strings, which is fine, because they're just names), and fetch their values from Systems Manager at runtime --- i.e., when the Lambda is invoked sometime later --- with the &lt;a href="https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-started-nodejs.html" rel="noopener noreferrer"&gt;AWS SDK for Node.js&lt;/a&gt;. This way, any updates made by the &lt;code&gt;shared-config&lt;/code&gt; team will be usable by our Lambda function &lt;em&gt;immediately&lt;/em&gt;, no redeployment necessary.&lt;/p&gt;

&lt;p&gt;So to finish things off, change to the &lt;code&gt;my-service&lt;/code&gt; folder you created earlier and generats a third and final project, this one with TypeScript, again accepting the defaults to create a &lt;code&gt;dev&lt;/code&gt; stack for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../my-service
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi new aws-typescript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the contents of &lt;code&gt;index.ts&lt;/code&gt; with the following program. As before, we're using &lt;code&gt;StackReference&lt;/code&gt;s to read our parameter names from the &lt;code&gt;shared-config&lt;/code&gt; stack --- except here, because &lt;code&gt;StackRerefence&lt;/code&gt;s are provided as &lt;a href="https://www.pulumi.com/docs/intro/concepts/inputs-outputs" rel="noopener noreferrer"&gt;&lt;code&gt;pulumi.Output&lt;/code&gt;&lt;/a&gt;s, which can't be serialized into the Lambda, we'll need to use &lt;code&gt;.get()&lt;/code&gt; to unwrap the parameter name into a string so we can use it at runtime with the AWS SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/pulumi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/aws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Get the names of the parameters we care about from the shared-config stack.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StackReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cnunciato/shared-config/dev&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- Your name here.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;motdParamRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requireOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;motd_param_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;motdSecretParamRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requireOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;motd_secret_param_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CallbackFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lambda&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&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;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ManagedPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LambdaFullAccess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="c1"&gt;// Give the Lambda read-only access to AWS Systems Manager.&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;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ManagedPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AmazonSSMReadOnlyAccess&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &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 class="c1"&gt;// Use the AWS SDK for JavaScript to fetch parameter values from Systems Manager.&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ssm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SSM&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Fetch the message of the day.&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;motdParam&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ssm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getParameter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;motdParamRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;  &lt;span class="c1"&gt;// &amp;lt;-- Use .get() for you runtime access to Output values.&lt;/span&gt;
        &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Fetch the secret message of the day.&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;motdSecretParam&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ssm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getParameter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;motdSecretParamRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="na"&gt;WithDecryption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="c1"&gt;// Return the result.&lt;/span&gt;
            &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;motd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;motdParam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;motd_secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;motdSecretParam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;Value&lt;/span&gt;
            &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lambdaUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FunctionUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lambda-url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;callback&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 class="na"&gt;authorizationType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NONE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Export the publicly accessible URL of the service.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lambdaUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you're ready, deploy the &lt;code&gt;my-service&lt;/code&gt; stack with &lt;code&gt;pulumi up&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up
...

Updating &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;

     Type                             Name             Status
 +   pulumi:pulumi:Stack              my-service-dev   created
 +   ├─ aws:iam:Role                  lambda           created
 +   ├─ aws:lambda:Function           lambda           created
 +   ├─ aws:iam:RolePolicyAttachment  lambda-b63d666c  created
 +   ├─ aws:iam:RolePolicyAttachment  lambda-b5aeb6b6  created
 +   └─ aws:lambda:FunctionUrl        lambda-url       created

Outputs:
    url: &lt;span class="s2"&gt;"https://6y3vkpktyybylhua266bu7yplu0dgqgv.lambda-url.us-west-2.on.aws/"&lt;/span&gt;

Resources:
    + 6 created

Duration: 19s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the deployment with a request to the Lambda function's public endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="si"&gt;$(&lt;/span&gt;pulumi stack output url&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"motd"&lt;/span&gt;: &lt;span class="s2"&gt;"Hello from Pulumi!"&lt;/span&gt;,
  &lt;span class="s2"&gt;"motd_secret"&lt;/span&gt;: &lt;span class="s2"&gt;"Ssh! This is a secret."&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tells you the runtime lookup is working, which is great.&lt;/p&gt;

&lt;p&gt;Now try updating the message of the day. Change to the &lt;code&gt;shared-config&lt;/code&gt; folder, use &lt;code&gt;pulumi config set&lt;/code&gt; to update the stack's configured value, and finally, run &lt;code&gt;pulumi up&lt;/code&gt; to write the new value to Systems Manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../shared-config
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;motd &lt;span class="s1"&gt;'Hello *again* from Pulumi!'&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up
...

Updating &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;

     Type                  Name               Plan       Info
     pulumi:pulumi:Stack   shared-config-dev
 ~   └─ aws:ssm:Parameter  motd_param         updated    &lt;span class="o"&gt;[&lt;/span&gt;diff: ~value]

Resources:
    ~ 1 updated
    2 unchanged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all goes well, you should be able to invoke the Lambda again and see the new value immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../my-service
&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="si"&gt;$(&lt;/span&gt;pulumi stack output url&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"motd"&lt;/span&gt;: &lt;span class="s2"&gt;"Hello *again* from Pulumi!"&lt;/span&gt;,
  &lt;span class="s2"&gt;"motd_secret"&lt;/span&gt;: &lt;span class="s2"&gt;"Ssh! This is a secret."&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with that, we're done. You can tear everything down with &lt;code&gt;pulumi destroy&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ..
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; my-service destroy
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; my-website destroy
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; shared-config destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping up, and next steps
&lt;/h2&gt;

&lt;p&gt;Managing configuration at scale is a much bigger topic than we could possibly cover in a blog post, but hopefully you've learned enough here to fuel some experimentation of your own. You'll find the &lt;a href="https://github.com/cnunciato/shared-config-with-pulumi-and-ssm" rel="noopener noreferrer"&gt;full source code for this walkthrough on GitHub&lt;/a&gt; as well.&lt;/p&gt;

&lt;p&gt;Additionally, as an alternative to Parameter Store, you might also want to check out &lt;a href="https://aws.amazon.com/secrets-manager/" rel="noopener noreferrer"&gt;AWS Secrets Manager&lt;/a&gt;. It's built specifically for managing secrets, and gives you additional capabilities like rotation and auditing, so depending on your use case, it could be a better fit for certain scenarios.&lt;/p&gt;

&lt;p&gt;Happy configuring!&lt;/p&gt;

</description>
      <category>pulumi</category>
      <category>aws</category>
      <category>cloud</category>
      <category>yaml</category>
    </item>
    <item>
      <title>Test-Driven Infrastructure Development with Pulumi and Jest</title>
      <dc:creator>Christian Nunciato</dc:creator>
      <pubDate>Tue, 14 Jun 2022 23:30:09 +0000</pubDate>
      <link>https://forem.com/pulumi/test-driven-infrastructure-development-with-pulumi-and-jest-121f</link>
      <guid>https://forem.com/pulumi/test-driven-infrastructure-development-with-pulumi-and-jest-121f</guid>
      <description>&lt;p&gt;When I was a kid growing up in Southern California, there was a phone number you could call to find out what time it was. It was a local number, 853-1212 (easy to remember as the arrangement of the numbers on the keypad made a capital T), and I used it all the time, to set my watch, adjust the alarm clock, fix the display on the VCR. I don't recall the last time I used it, probably sometime in the mid '90s, but I do remember clearly the sound of &lt;a href="https://telephoneworld.org/telephone-sounds/modern-north-american-telephone-sounds/time-temperature-weather-forecast-recordings/" rel="noopener noreferrer"&gt;the voice at the other end of the line&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Services like these were called &lt;a href="https://en.wikipedia.org/wiki/Speaking_clock" rel="noopener noreferrer"&gt;speaking clocks&lt;/a&gt; (although I never called them that; to me, it was just "calling Time"), and they date back to the early 1930s or so. In the U.S., most were powered by a machine called an &lt;a href="https://en.wikipedia.org/wiki/Audichron" rel="noopener noreferrer"&gt;Audichron&lt;/a&gt; that used mechanical drums to play back the time of day, often with a little advertisement or the current temperature to go along with it.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/0IHzWWMzqmI"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;By the mid-2000s, though, most of these locally run services had been shut down --- by then, we had clocks baked into our phones --- and today, only a handful of Audichrons survive. Thanks to the National Institute of Standards and Technology, however, &lt;a href="https://www.theatlantic.com/technology/archive/2016/06/remember-when-you-could-call-the-time/488273/" rel="noopener noreferrer"&gt;you can still call a phone number to get the time&lt;/a&gt;, and while the voice might not be the same, and long distance rates will apply, it's definitely there, and you can use it. So on the off chance you happen to find yourself with no idea what time it is and only an analog phone line in reach, fear not --- old-school telephone tech has your back. For now. Assuming you remember the number.&lt;/p&gt;

&lt;p&gt;Recalling all this stuff did make me wonder, though, what a more modern version of a speaking clock might look like. So in this post, we're going to build one ourselves. We won't use an actual phone number, but we will use Pulumi and AWS --- and because we want to do it &lt;em&gt;right&lt;/em&gt;, we'll take a test-driven approach to developing the infrastructure with &lt;a href="https://jestjs.io" rel="noopener noreferrer"&gt;Jest, the JavaScript testing framework&lt;/a&gt;. We'll use TypeScript and Node.js for everything, focus on &lt;a href="https://www.pulumi.com/docs/guides/testing/unit" rel="noopener noreferrer"&gt;unit tests&lt;/a&gt;, and when we're done, we'll have a single, serverless, browser-friendly HTTPS endpoint that returns an MP3 audio stream that speaks the current time.&lt;/p&gt;

&lt;p&gt;Let's get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sketching it out
&lt;/h2&gt;

&lt;p&gt;The first thing we'll need is a runtime environment --- someplace to run some server-side JavaScript that can render and deliver an audio file. Until recently, the easiest way to get an HTTP endpoint up and running on AWS has generally been with &lt;a href="https://aws.amazon.com/lambda/" rel="noopener noreferrer"&gt;AWS Lambda&lt;/a&gt; and &lt;a href="https://aws.amazon.com/api-gateway/" rel="noopener noreferrer"&gt;API Gateway&lt;/a&gt;, using Lambda to run the requisite code and API Gateway to expose the Lambda to the internet. Pulumi Crosswalk actually makes this &lt;a href="https://www.pulumi.com/docs/guides/crosswalk/aws/api-gateway" rel="noopener noreferrer"&gt;really easy&lt;/a&gt;, too --- but with the &lt;a href="https://www.pulumi.com/blog/lambda-urls-launch" rel="noopener noreferrer"&gt;release of AWS Lambda Function URLs&lt;/a&gt; this April, we now have another option, one that doesn't need API Gateway at all.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html" rel="noopener noreferrer"&gt;Lambda Function URL&lt;/a&gt; is just what it sounds like: a URL that exposes a Lambda function. Specifically, it's an AWS cloud resource that consists of a few properties that tell AWS whether to allow anonymous access to the function or to &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html#urls-auth-iam" rel="noopener noreferrer"&gt;protect it with AWS IAM&lt;/a&gt;, and optionally, you can also provide a few &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html#urls-cors" rel="noopener noreferrer"&gt;cross-origin resource-sharing (CORS) rules&lt;/a&gt; to provide (or restrict) access by websites running on other domains.&lt;/p&gt;

&lt;p&gt;For an app like this one, a Function URL is a good fit, as it's simple, quick to deploy (and update), and it gives us all the control we need to get the job done. Architecturally, then, our little app will look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Farchitecture.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Farchitecture.svg" alt="Diagram showing a Node.js Lambda function proxied by a Lambda Function URL" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That about covers the infrastructure --- but what about the sound?&lt;/p&gt;

&lt;p&gt;As it happens, &lt;a href="https://translate.google.com/" rel="noopener noreferrer"&gt;Google Translate&lt;/a&gt; offers a publicly accessible text-to-speech API that can, as long as you don't overuse it, render a string to MP3 audio. We can use that API to generate the time-of-day message, tack the sound of a beep onto the end of it (for maximum nostalgic effect), and voilà: instant, infinitely scalable, cloud-native speaking clock.&lt;/p&gt;

&lt;p&gt;Plan in place, we can kick things off by creating a new Pulumi project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a new TypeScript project
&lt;/h2&gt;

&lt;p&gt;Start by creating a new AWS TypeScript project &lt;a href="https://dev.to/docs/get-started/aws/create-project?language=nodejs"&gt;in the usual way&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;audichron-2022 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;audichron-2022
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi new aws-typescript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step through the prompts to create a new &lt;a href="https://www.pulumi.com/docs/intro/concepts/stack" rel="noopener noreferrer"&gt;stack&lt;/a&gt; (you'll only need one stack for this project), and when the new-project wizard completes, clear out the contents of &lt;code&gt;index.ts&lt;/code&gt; entirely, as we'll be building this program entirely from the ground up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and configure Jest
&lt;/h2&gt;

&lt;p&gt;Since we're working with TypeScript, we can use &lt;a href="https://github.com/kulshekhar/ts-jest" rel="noopener noreferrer"&gt;&lt;code&gt;ts-jest&lt;/code&gt;&lt;/a&gt;, which conveniently brings Jest along for the ride:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;ts-jest @types/jest &lt;span class="nt"&gt;--save-dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jest requires a bit of &lt;a href="https://jestjs.io/docs/configuration" rel="noopener noreferrer"&gt;configuration&lt;/a&gt;, so add a new file at the root of the project, called &lt;code&gt;jest.config.ts&lt;/code&gt;, with the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Config&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@jest/types&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InitialOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;preset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ts-jest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Jest now installed and configured, and its helpers (like &lt;code&gt;describe&lt;/code&gt; and &lt;code&gt;expect&lt;/code&gt;) declared globally, you can create a new spec file to hold your tests. Add another file at the root of the project called &lt;code&gt;index.spec.ts&lt;/code&gt;, and give it the following temporary content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My speaking clock&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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 class="nf"&gt;it &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;works&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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 class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toBeTruthy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly, in &lt;code&gt;package.json&lt;/code&gt;, add a &lt;code&gt;scripts&lt;/code&gt; block with a helper to make running the tests a little easier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audichron-2022&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;devDependencies&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@types/node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;^14&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ts-jest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;^28.0.3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scripts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now run the tests, just to make sure you've got everything wired up correctly. If all goes well, you should see that the temporary spec we just added happily passes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;test

&lt;/span&gt;PASS  ./index.spec.ts
  My speaking clock
    ✓ works
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, it's time to get to start writing some real tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with some failing tests
&lt;/h2&gt;

&lt;p&gt;As devoted practitioners of &lt;a href="https://en.wikipedia.org/wiki/Test-driven_development" rel="noopener noreferrer"&gt;test-driven development&lt;/a&gt;, we're going to start by writing some unit tests --- specifically, some &lt;em&gt;failing&lt;/em&gt; unit tests that we can fix by writing the Pulumi code to make them pass.&lt;/p&gt;

&lt;p&gt;Recall that our design requires just two cloud resources: a Lambda function and a Lambda function URL. For the function itself, we'll use the high-level &lt;a href="https://dev.tohttps"&gt;&lt;code&gt;aws.lambda.CallbackFunction&lt;/code&gt;&lt;/a&gt; resource, one of my favorites for managing for Lambdas because it requires only one property: an inline JavaScript function to handle the event that triggers the Lambda.&lt;/p&gt;

&lt;p&gt;For the URL resource --- the eventual triggerer of that event ---  you'll use an &lt;a href="{https://www.pulumi.com/registry/packages/aws/api-docs/lambda/functionurl"&gt;&lt;code&gt;aws.lambda.FunctionURL&lt;/code&gt;&lt;/a&gt; configured to make the Lambda publicly accessible (i.e., available to anyone on the internet) and embeddable on any domain, making it easy, for example, to embed the URL as the &lt;code&gt;src&lt;/code&gt; attribute of an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio" rel="noopener noreferrer"&gt;HTML5 &lt;code&gt;audio&lt;/code&gt; element&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So to get things going, we'll need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Configure Pulumi to &lt;a href="https:///docs/guides/testing/unit#add-mocks" rel="noopener noreferrer"&gt;mock AWS resources&lt;/a&gt;. We're writing unit tests, after all, and we want them to be fast, so we'll need to prevent those tests from provisioning any real cloud infrastructure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Import your Pulumi resource declarations --- the function and the function URL --- into &lt;code&gt;index.spec.ts&lt;/code&gt; so you can reference them in tests.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In that light, replace the contents of &lt;code&gt;index.spec.ts&lt;/code&gt; with the following code, which, after some setup to switch Pulumi into unit-testing mode, contains just one test: a check to make sure that the function URL is configured with the right &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html" rel="noopener noreferrer"&gt;&lt;code&gt;authorizationType&lt;/code&gt;&lt;/a&gt;. I've included some comments to help explain what each line is doing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/pulumi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My speaking clock&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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 class="c1"&gt;// Define the infra variable as a type whose shape matches that of the&lt;/span&gt;
    &lt;span class="c1"&gt;// to-be-defined resources module.&lt;/span&gt;
    &lt;span class="c1"&gt;// https://www.typescriptlang.org/docs/handbook/2/typeof-types.html&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./resources&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;beforeAll&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 class="c1"&gt;// Put Pulumi in unit-test mode, mocking all calls to cloud-provider APIs.&lt;/span&gt;
        &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setMocks&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;

            &lt;span class="c1"&gt;// Mock requests to provision cloud resources and return a canned response.&lt;/span&gt;
            &lt;span class="na"&gt;newResource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MockResourceArgs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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 class="c1"&gt;// Here, we're returning a same-shaped object for all resource types.&lt;/span&gt;
                &lt;span class="c1"&gt;// We could, however, use the arguments passed into this function to&lt;/span&gt;
                &lt;span class="c1"&gt;// customize the mocked-out properties of a particular resource based&lt;/span&gt;
                &lt;span class="c1"&gt;// on its type. See the unit-testing docs for details:&lt;/span&gt;
                &lt;span class="c1"&gt;// https://www.pulumi.com/docs/guides/testing/unit&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;args&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 class="s2"&gt;-id`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;};&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;

            &lt;span class="c1"&gt;// Mock function calls and return whatever input properties were provided.&lt;/span&gt;
            &lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MockCallArgs&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 class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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 class="c1"&gt;// Dynamically import the resources module.&lt;/span&gt;
        &lt;span class="c1"&gt;// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports&lt;/span&gt;
        &lt;span class="nx"&gt;infra&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./resources&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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 class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is publicly accessible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&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 class="nx"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeURL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorizationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authType&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authType&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NONE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can't quite run these tests just yet, though, as they rely on a module called &lt;code&gt;resources&lt;/code&gt; that we haven't yet defined. Let's do that now by creating a new file, &lt;code&gt;resources.ts&lt;/code&gt;, containing some stubs for the cloud resources we've elected to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/pulumi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/aws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CallbackFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;time-function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&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 class="c1"&gt;// Leave the callback implementation empty for now.&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FunctionUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;time-url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timeFunction&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 class="na"&gt;authorizationType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Leave this empty, too. (We want our first test to fail.)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now run the tests, and you should see that the public-accessibility test fails --- which shouldn't be surprising, as we haven't yet supplied an acceptable value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;test

&lt;/span&gt;FAIL  ./index.spec.ts
  My speaking clock
    &lt;span class="k"&gt;function &lt;/span&gt;URL
      ✕ is publicly accessible

  ● My speaking clock › &lt;span class="k"&gt;function &lt;/span&gt;URL › is publicly accessible

    expect&lt;span class="o"&gt;(&lt;/span&gt;received&lt;span class="o"&gt;)&lt;/span&gt;.toBe&lt;span class="o"&gt;(&lt;/span&gt;expected&lt;span class="o"&gt;)&lt;/span&gt; // Object.is equality

    Expected: &lt;span class="s2"&gt;"NONE"&lt;/span&gt;
    Received: &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's turn that sad-looking ❌ into a ✅.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the tests pass
&lt;/h2&gt;

&lt;p&gt;Update the &lt;code&gt;FunctionUrl&lt;/code&gt; resource to change its &lt;code&gt;authorizationType&lt;/code&gt; input to &lt;code&gt;NONE&lt;/code&gt; (meaning for anonymous access, no authorization required):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;export const timeURL = new aws.lambda.FunctionUrl("time-url", {
&lt;/span&gt;    functionName: timeFunction.name,
&lt;span class="gd"&gt;-   authorizationType: "",
&lt;/span&gt;&lt;span class="gi"&gt;+   authorizationType: "NONE",
&lt;/span&gt;});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you do that, you should be able to run the test suite again, and this time, see it pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;test

&lt;/span&gt;PASS  ./index.spec.ts
  My speaking clock
    &lt;span class="k"&gt;function &lt;/span&gt;URL
      ✓ is publicly accessible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Excellent: you're now well on your way to writing more tests with Jest --- and we'll do that in a moment. But first, let's digress momentarily to explore a few other ways of writing Jest specs with Pulumi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tidy up the specs
&lt;/h2&gt;

&lt;p&gt;If you've written JavaScript tests before, particularly with tools like Jest, &lt;a href="https://jasmine.github.io/" rel="noopener noreferrer"&gt;Jasmine&lt;/a&gt;, and &lt;a href="https://mochajs.org/" rel="noopener noreferrer"&gt;Mocha&lt;/a&gt;, there's a good chance you've gotten used to writing those tests in a particular way. And if you're like me, you might've raised a bit of an eyebrow when you saw how that first test was written. Here it is again, this time with comments that capture the questions I might've had myself if I were seeing this code for the first time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is publicly accessible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&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 class="nx"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeURL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorizationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// Why do I have to use .apply() here?&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                                       &lt;span class="c1"&gt;// What happens if I don't try/catch?&lt;/span&gt;
            &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NONE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// Is it possible to use expect() by itself?&lt;/span&gt;
            &lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                                 &lt;span class="c1"&gt;// Do I have to call done()? What if I don't?&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                             &lt;span class="c1"&gt;// How can I make these tests more readable?&lt;/span&gt;
            &lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                              &lt;span class="c1"&gt;// Is all of this code necessary?&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a perfectly valid test, of course; we know because we just ran it. Still, you might wonder, especially since we're only checking one property, whether this test might be written a bit more concisely --- something more like this, maybe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is publicly accessible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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 class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeURL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorizationType&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NONE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Alas, nope.&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main reason is the asynchronous nature of the objects we're testing. An &lt;code&gt;aws.lambda.FunctionUrl&lt;/code&gt;, after all, isn't just a plain ol' JavaScript object --- it's a &lt;a href="https:///docs/intro/concepts/resources" rel="noopener noreferrer"&gt;Pulumi &lt;code&gt;CustomResource&lt;/code&gt;&lt;/a&gt;, a representation of an eventual resource running in the cloud, with properties that may not be known until sometime after the resource has been deployed. While it's true we provided &lt;code&gt;authorizationType&lt;/code&gt; as a regular JavaScript &lt;code&gt;string&lt;/code&gt;, by the time we attempt to read it in the test, it's been transformed into a &lt;a href="https://www.pulumi.com/docs/intro/concepts/inputs-outputs#apply" rel="noopener noreferrer"&gt;&lt;code&gt;pulumi.Output&amp;lt;string&amp;gt;&lt;/code&gt;&lt;/a&gt; --- an asynchronous value, like a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise" rel="noopener noreferrer"&gt;promise&lt;/a&gt;, that has to be unwrapped with a call to &lt;code&gt;apply()&lt;/code&gt; before it can be read.&lt;/p&gt;

&lt;p&gt;However, just adding &lt;code&gt;apply&lt;/code&gt; and calling &lt;code&gt;expect&lt;/code&gt; from within its callback doesn't quite work, either:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is publicly accessible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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 class="nx"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeURL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorizationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authType&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authType&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NONE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Also nope.&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although that might not be obvious at first, since on the surface, the test &lt;em&gt;appears&lt;/em&gt; to pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;test

&lt;/span&gt;PASS  ./index.spec.ts
  My speaking clock
    &lt;span class="k"&gt;function &lt;/span&gt;URL
      ✓ is publicly accessible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It isn't until you force the test to fail that it becomes clearer something's gone sideways. Try changing the expected &lt;code&gt;authorizationType&lt;/code&gt; to something other than &lt;code&gt;NONE&lt;/code&gt; and you'll see what I mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;test

&lt;/span&gt;PASS  ./index.spec.ts
  My &lt;span class="nb"&gt;time &lt;/span&gt;app
    &lt;span class="k"&gt;function &lt;/span&gt;URL
      ✓ is publicly accessible

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total

&lt;span class="o"&gt;[&lt;/span&gt;UnhandledPromiseRejection: This error originated either by throwing inside of an async &lt;span class="k"&gt;function
&lt;/span&gt;without a catch block, or by rejecting a promise which was not handled with .catch&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; The promise
rejected with the reason &lt;span class="s2"&gt;"Error: expect(received).toBe(expected) // Object.is equality

Expected: "&lt;/span&gt;AWS_IAM&lt;span class="s2"&gt;"
Received: "&lt;/span&gt;NONE&lt;span class="s2"&gt;""&lt;/span&gt;.] &lt;span class="o"&gt;{&lt;/span&gt;
  code: &lt;span class="s1"&gt;'ERR_UNHANDLED_REJECTION'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All is not lost, though. We do have other options, and that &lt;code&gt;UnhandledPromiseRejection&lt;/code&gt; offers a bit of a clue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn outputs into promises
&lt;/h2&gt;

&lt;p&gt;Jest actually has &lt;a href="https://jestjs.io/docs/asynchronous" rel="noopener noreferrer"&gt;built-in support for writing asynchronous tests with promises&lt;/a&gt;, and there are a couple of ways you can write specs that use them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have the function passed to &lt;code&gt;it&lt;/code&gt; (or its alias, &lt;code&gt;test&lt;/code&gt;) return a promise containing your expectation&lt;/li&gt;
&lt;li&gt;Pass an &lt;code&gt;async&lt;/code&gt; function to &lt;code&gt;it&lt;/code&gt; and &lt;code&gt;await&lt;/code&gt; the promise in the usual way&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's only one problem: &lt;code&gt;pulumi.Output&lt;/code&gt;s may indeed be promise-&lt;em&gt;like&lt;/em&gt;, but they aren't actual promises.&lt;/p&gt;

&lt;p&gt;They can, however, be wrapped in promises pretty easily. I like the way Pulumi community member &lt;a href="https://github.com/aviflax" rel="noopener noreferrer"&gt;@aviflax&lt;/a&gt; handled this &lt;a href="https://github.com/pulumi/pulumi/issues/5924#issuecomment-897938240" rel="noopener noreferrer"&gt;with a little utility function&lt;/a&gt; that wraps the &lt;code&gt;Output&lt;/code&gt; and preserves the type of its underlying value. Here's that function, followed by a couple of examples that use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Convert a pulumi.Output to a promise of the same type.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;promiseOf&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Our test, rewritten to have it() return a Promise&amp;lt;string&amp;gt;.&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is publicly accessible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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 class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;promiseOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeURL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorizationType&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authType&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authType&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NONE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// The same test, rewritten to use async/await.&lt;/span&gt;
&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is publicly accessible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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 class="c1"&gt;// &amp;lt;-- Don't miss that async!&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;promiseOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeURL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorizationType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authType&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NONE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either approach works equally well, but I'm slightly more partial to the latter for its conciseness. So for now, update &lt;code&gt;index.spec.ts&lt;/code&gt; to add the &lt;code&gt;promiseOf&lt;/code&gt; helper and adjust the test accordingly, then run the suite one more time to make sure all's well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="c1"&gt;// Convert a pulumi.Output to a promise of the same type.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;promiseOf&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My speaking clock&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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 class="c1"&gt;// ...&lt;/span&gt;

    &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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 class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is publicly accessible&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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 class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;promiseOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeURL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorizationType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authType&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NONE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
     &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assuming you're back in ✅ territory, let's move on so we can finish things off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finish the app
&lt;/h2&gt;

&lt;p&gt;We're in good shape: we've got scaffolding for our Jest-powered unit tests and we're generally happy with how they look, so it's time to round out the rest of the app and its tests so we can deploy. We'll go through this last part briskly, as we've covered most of the important stuff at this point, and pause only briefly to point out what's important as we go.&lt;/p&gt;

&lt;p&gt;Here's the plan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We'll add one more test to make sure the function URL is CORS-friendly&lt;/li&gt;
&lt;li&gt;We'll add another test to make sure the URL resource is bound to the right Lambda function&lt;/li&gt;
&lt;li&gt;We'll add the Lambda code to generate and return the audio file&lt;/li&gt;
&lt;li&gt;We'll deploy, and bask in the warm glow of nostalgia&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's get to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add the remaining tests
&lt;/h3&gt;

&lt;p&gt;To &lt;code&gt;index.spec.ts&lt;/code&gt;, add two more tests, one to ensure the function is accessible to browsers running &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" rel="noopener noreferrer"&gt;on any domain&lt;/a&gt;, the other to verify the function URL resource is exposing the right function (if we didn't, it'd be easy to change that reference by mistake and accidentally expose the wrong function):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My speaking clock&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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 class="c1"&gt;// ...&lt;/span&gt;

    &lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&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 class="c1"&gt;// ...&lt;/span&gt;

        &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is CORS-friendly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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 class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;promiseOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeURL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authType&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;allowOrigins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="na"&gt;allowMethods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is bound to the right Lambda function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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 class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeFuncName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;promiseOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeFunction&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 class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeURLFunc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;promiseOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeURL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeFuncName&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeURLFunc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the tests, and you should see one failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;test

&lt;/span&gt;FAIL  ./index.spec.ts &lt;span class="o"&gt;(&lt;/span&gt;14.256 s&lt;span class="o"&gt;)&lt;/span&gt;
  My speaking clock
    &lt;span class="k"&gt;function &lt;/span&gt;URL
      ✓ is publicly accessible &lt;span class="o"&gt;(&lt;/span&gt;3022 ms&lt;span class="o"&gt;)&lt;/span&gt;
      ✕ is CORS-friendly &lt;span class="o"&gt;(&lt;/span&gt;3 ms&lt;span class="o"&gt;)&lt;/span&gt;
      ✓ is bound to the right Lambda &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;1 ms&lt;span class="o"&gt;)&lt;/span&gt;

    Expected: &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"allowMethods"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;, &lt;span class="s2"&gt;"allowOrigins"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="o"&gt;]}&lt;/span&gt;
    Received: undefined

Tests:       1 failed, 2 passed, 3 total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick addition to the &lt;code&gt;FunctionUrl&lt;/code&gt; in &lt;code&gt;resources.ts&lt;/code&gt; should get you to green:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;// ...
&lt;span class="p"&gt;export const timeURL = new aws.lambda.FunctionUrl("time-url", {
&lt;/span&gt;    functionName: timeFunction.name,
    authorizationType: "NONE",
&lt;span class="gi"&gt;+   cors: {
+       allowOrigins: ["*"],
+       allowMethods: ["GET"],
+   },
&lt;/span&gt;});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;test

&lt;/span&gt;PASS  ./index.spec.ts
  My speaking clock
    &lt;span class="k"&gt;function &lt;/span&gt;URL
      ✓ is publicly accessible
      ✓ is CORS-friendly
      ✓ is bound to the right Lambda &lt;span class="k"&gt;function

&lt;/span&gt;Tests:       3 passed, 3 total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it for the tests! Now for the final additions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Produce the audio message
&lt;/h3&gt;

&lt;p&gt;The last thing we'll need to do is render some audio. For this, we'll need to communicate with Google Translate, our text-to-speech provider of choice, and that means we'll need an HTTP client. I generally like &lt;a href="https://www.npmjs.com/package/node-fetch" rel="noopener noreferrer"&gt;&lt;code&gt;node-fetch&lt;/code&gt;&lt;/a&gt;, so let's go with that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save&lt;/span&gt; node-fetch@2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, finish the program by importing &lt;code&gt;node-fetch&lt;/code&gt; into &lt;code&gt;resources.ts&lt;/code&gt;, adding a function to produce a speech-friendly string indicating the current time and updating the &lt;code&gt;CallbackFunction&lt;/code&gt;'s &lt;code&gt;callback&lt;/code&gt; property to send the string to Google Translate to produce the desired result. Here's the complete file, with comments, for reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/pulumi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/aws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node-fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Convert the current time into a speech-friendly string.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSpeechText&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSeconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;America/Los_Angeles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;localHour&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHours&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;localMinute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMinutes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localHour&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;localHour&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;localHour&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localMinute&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`oh &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;localMinute&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;localMinute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// So 2:03 -&amp;gt; "two-oh-three"&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSeconds&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`At the tone, the time will be &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. And &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; seconds.`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeFunction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CallbackFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;time-function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Update the Lambda callback body to convert the current time into an MP3 file.&lt;/span&gt;
    &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &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 class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSpeechText&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;speechURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://translate.google.com/translate_tts?ie=UTF-8&amp;amp;q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;textLen=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;tl=en&amp;amp;client=tw-ob`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;beepURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.pulumi.com/uploads/beep.mp3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ttsResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;speechURL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;beepResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;beepURL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;speech&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ttsResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;beep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;beepResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Tack a beep onto the end of the audio returned from Google Translate, then&lt;/span&gt;
        &lt;span class="c1"&gt;// render the whole thing as a base-64 encoded string.&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;speech&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;beep&lt;/span&gt;&lt;span class="p"&gt;)]).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Return an appropriately shaped HTTP response.&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio/mpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="na"&gt;isBase64Encoded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FunctionUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;time-url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;functionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;timeFunction&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 class="na"&gt;authorizationType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NONE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;allowOrigins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;allowMethods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Export the public URL of our shiny new service.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;audioURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;timeURL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, open &lt;code&gt;index.ts&lt;/code&gt; (which should still be empty) and add a couple of lines to import the &lt;code&gt;resources&lt;/code&gt; module and export the function URL as a Pulumi &lt;a href="https://www.pulumi.com/docs/intro/concepts/stack#outputs" rel="noopener noreferrer"&gt;stack output&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./resources&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;audioURL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./resources&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it. We're ready to deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy the service
&lt;/h2&gt;

&lt;p&gt;Doublecheck your AWS credentials are &lt;a href="https://www.pulumi.com/registry/packages/aws/installation-configuration" rel="noopener noreferrer"&gt;configured&lt;/a&gt;, then deploy the new service with a single &lt;code&gt;pulumi up&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up

...
Updating &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;

View Live: https://app.pulumi.com/cnunciato/audichron-2022/dev/updates/1

     Type                             Name                    Status
 +   pulumi:pulumi:Stack              audichron-2022-dev      created
 +   ├─ aws:iam:Role                  time-function           created
 +   ├─ aws:lambda:Function           time-function           created
 +   ├─ aws:iam:RolePolicyAttachment  time-function-7cd09230  created
 +   ├─ aws:iam:RolePolicyAttachment  time-function-a1de8170  created
 +   ├─ aws:iam:RolePolicyAttachment  time-function-1b4caae3  created
 +   ├─ aws:iam:RolePolicyAttachment  time-function-019020e7  created
 +   ├─ aws:iam:RolePolicyAttachment  time-function-4aaabb8e  created
 +   ├─ aws:iam:RolePolicyAttachment  time-function-74d12784  created
 +   ├─ aws:iam:RolePolicyAttachment  time-function-e1a3786d  created
 +   ├─ aws:iam:RolePolicyAttachment  time-function-6c156834  created
 +   ├─ aws:iam:RolePolicyAttachment  time-function-b5aeb6b6  created
 +   └─ aws:lambda:FunctionUrl        time-url                created

Outputs:
    audioURL: &lt;span class="s2"&gt;"https://52rjmmybwfwcud3nxxhosznpum0cycbw.lambda-url.us-west-2.on.aws/"&lt;/span&gt;

Resources:
    + 13 created

Duration: 20s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the update completes, open the URL in your browser of choice, and you should hear the message you've been waiting for --- one that might even make &lt;a href="https://en.wikipedia.org/wiki/Pat_Fleet" rel="noopener noreferrer"&gt;Pat Fleet&lt;/a&gt; proud:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;open &lt;span class="si"&gt;$(&lt;/span&gt;pulumi stack output audioURL&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you're ready, you can tear everything down with a &lt;code&gt;pulumi destroy&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Beyond just having a nifty (and admittedly rather silly) new way to tell time, you should have a much better sense at this point of how you can use Pulumi with Jest to write better, safer infrastructure code.&lt;/p&gt;

&lt;p&gt;From here, there's a bunch more you might think about next: writing more tests to cover the code we just added, &lt;a href="https://www.pulumi.com/docs/guides/testing" rel="noopener noreferrer"&gt;exploring some additional flavors of testing&lt;/a&gt; in the docs, or &lt;a href="https://github.com/pulumi/examples" rel="noopener noreferrer"&gt;having a look at a few examples&lt;/a&gt;. You'll find the &lt;a href="https://github.com/cnunciato/pulumi-jest-unit-testing-example" rel="noopener noreferrer"&gt;full source for this walkthrough up on GitHub&lt;/a&gt; as well.&lt;/p&gt;

&lt;p&gt;Happy testing!&lt;/p&gt;

</description>
      <category>pulumi</category>
      <category>javascript</category>
      <category>jest</category>
      <category>aws</category>
    </item>
    <item>
      <title>Fullstack Pulumi: Deploying the MERN Stack on DigitalOcean</title>
      <dc:creator>Christian Nunciato</dc:creator>
      <pubDate>Mon, 14 Mar 2022 17:33:32 +0000</pubDate>
      <link>https://forem.com/pulumi/fullstack-pulumi-deploying-the-mern-stack-on-digitalocean-40mk</link>
      <guid>https://forem.com/pulumi/fullstack-pulumi-deploying-the-mern-stack-on-digitalocean-40mk</guid>
      <description>&lt;p&gt;As a developer, I get lots of ideas for web apps—little things, mostly: nifty ways to keep track of my kids' allowances, habit trackers, shopping lists. Most of them, however, never see the light of day, and not just because I'm lazy; I also tend to get hung up trying to decide what to use for the technology stack.&lt;/p&gt;

&lt;p&gt;And as a JavaScript developer, I certainly have options—too many, in fact, and that's part of the problem. Having roughly a hundred and fifty million libraries and frameworks to choose from is definitely better than having none, but at the same time, all that choice can make the actual choosing rather difficult. Which is why, when I just want to get something done, I'll often reach for a combination of tools known as &lt;a href="https://www.mongodb.com/mern-stack" rel="noopener noreferrer"&gt;the MERN stack&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;MERN-stack apps are three-tier web apps built with &lt;a href="https://www.mongodb.com/" rel="noopener noreferrer"&gt;MongoDB&lt;/a&gt;, &lt;a href="https://expressjs.com/" rel="noopener noreferrer"&gt;Express&lt;/a&gt;, &lt;a href="https://reactjs.org/" rel="noopener noreferrer"&gt;React&lt;/a&gt;, and &lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt;. You can read all about them &lt;a href="https://www.mongodb.com/mern-stack" rel="noopener noreferrer"&gt;in the MongoDB docs&lt;/a&gt;, but the gist is that they allow you use one language—JavaScript (or TypeScript, if you like)—to manage all three layers of the application stack: the front end as a single-page app built statically with React, the back end as a REST API managed with Express, and the database as a collection of JSON-like documents with MongoDB. MERN might not &lt;em&gt;always&lt;/em&gt; the right tool for the job, but for the kinds of apps I tend to find myself building, it generally works out pretty well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkb8f3nn5ehm4gmf4pxrb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkb8f3nn5ehm4gmf4pxrb.png" alt="The tiers of a typical web application: front end, back end, and database" width="800" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Still, once I'm &lt;em&gt;finished&lt;/em&gt; building my app, I'm often faced with a whole other problem: figuring out how to get the app off of my laptop and onto the web.&lt;/p&gt;

&lt;p&gt;The cloud hasn't made this an easy task for developers. Choosing a cloud provider, deciding which resources to use (and how to use them), setting up networking, debugging permissions, navigating billing, and all the rest, can be overwhelming—and that's before you've given a single thought to anything having to do with automation or infrastructure as code. What we want, I think, is to be able to focus on our apps, and we're ready to ship, push our code to a repository and wait patiently for a URL to emerge that we can paste into a browser and have everything &lt;em&gt;just work&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Which is why I was so delighted when I discovered &lt;a href="https://www.digitalocean.com/products/app-platform" rel="noopener noreferrer"&gt;DigitalOcean's App Platform&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you've used DigitalOcean before, you know it's all about making infrastructure more accessible to developers. What you may not know, though, or at least I didn't myself until recently, is that you can do a lot more with DigitalOcean than just &lt;a href="https://www.digitalocean.com/products/droplets" rel="noopener noreferrer"&gt;virtual machines&lt;/a&gt;. A &lt;a href="https://www.digitalocean.com/blog/introducing-digitalocean-app-platform-reimagining-paas-to-make-it-simpler-for-you-to-build-deploy-and-scale-apps" rel="noopener noreferrer"&gt;fairly new&lt;/a&gt;, fully-managed platform service (think &lt;a href="https://heroku.com" rel="noopener noreferrer"&gt;Heroku&lt;/a&gt;), App Platform gives you a set of high-level abstractions built to align with the tiers of a typical web application, which means you can focus on what you care about most—your app—and leave the infrastructure and its management to someone else. It's a compelling option for anyone looking to deploy and manage any web application (MERN or otherwise), and as you'll see, with Pulumi and a little bit of code, you can easily do so without ever having to leave the comfort of your IDE.&lt;/p&gt;

&lt;p&gt;So let's build ourselves a MERN app and deploy it on DigitalOcean with Pulumi. Given the goal is to focus primarily on the infrastructure and how to code it, we'll start with a pre-baked web application (a simple grocery list), and we'll map its tiers to App Platform constructs and wire everything up with Pulumi.&lt;/p&gt;

&lt;h2&gt;
  
  
  First steps: setting up
&lt;/h2&gt;

&lt;p&gt;The code for this walkthrough is &lt;a href="https://github.com/cnunciato/fullstack-pulumi-mern-digitalocean" rel="noopener noreferrer"&gt;available as a template repository on GitHub&lt;/a&gt;, so if you want to follow along (and you should!), you should &lt;a href="https://github.com/cnunciato/fullstack-pulumi-mern-digitalocean/generate" rel="noopener noreferrer"&gt;grab a copy of your own&lt;/a&gt; to work with by forking the repository or creating a new one from the template. Once you've done that, you should also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/cnunciato/fullstack-pulumi-mern-digitalocean" rel="noopener noreferrer"&gt;Clone the repository&lt;/a&gt; to your local machine.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.pulumi.com/docs/get-started/install" rel="noopener noreferrer"&gt;Install Pulumi&lt;/a&gt; and &lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="//cloud.digitalocean.com/"&gt;Sign into DigitalOcean&lt;/a&gt; and obtain a &lt;a href="https://cloud.digitalocean.com/account/api/tokens" rel="noopener noreferrer"&gt;personal access token&lt;/a&gt; with read-write permissions.&lt;/li&gt;
&lt;li&gt;Grant DigitalOcean access to your GitHub repository by &lt;a href="https://cloud.digitalocean.com/apps" rel="noopener noreferrer"&gt;visiting the Apps page&lt;/a&gt;, choosing Create App, and following the steps to install DigitalOcean's GitHub app.&lt;/li&gt;
&lt;li&gt;Optionally, if you'd like to develop the application locally as well, &lt;a href="https://docs.mongodb.com/manual/tutorial/install-mongodb-on-os-x/" rel="noopener noreferrer"&gt;install and configure MongoDB Community Edition&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing to note: Since we'll be provisioning real DigitalOcean resources, there's a chance you could incur a slight cost for what you use. However, as we'll be using the least expensive plan settings available, and tearing everything down when we're through, that cost shouldn't amount to more than a few pennies or so.&lt;/p&gt;

&lt;p&gt;Let's get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloning and inspecting the repository
&lt;/h2&gt;

&lt;p&gt;Once you've cloned your copy of the template repository and navigated to the root, you'll see a couple of files and folders that look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;├── frontend
├── backend
└── package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;frontend&lt;/code&gt; folder contains the React application, and its job is to render the list of groceries and give you something to interact with (to add items, check them off, delete them, and so on). The scaffolding for the app was generated with a tool called &lt;a href="https://vitejs.dev/" rel="noopener noreferrer"&gt;Vite&lt;/a&gt;, and all of its logic—form fields, click handlers, API calls, etc.—is contained in &lt;code&gt;src/App.tsx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;backend&lt;/code&gt; folder contains the Express application that defines the REST API. It sets up four API &lt;a href="https://expressjs.com/en/guide/routing.html" rel="noopener noreferrer"&gt;routes&lt;/a&gt; to handle the &lt;a href="https://en.wikipedia.org/wiki/Create,_read,_update_and_delete" rel="noopener noreferrer"&gt;CRUD&lt;/a&gt; operations you'd expect in an app like this one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /api/items&lt;/code&gt; fetches all items from the database and returns them as a JSON array.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/items&lt;/code&gt; accepts a new item and writes it to the database.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PUT /api/items/:id&lt;/code&gt; updates an existing item (to toggle its checked/unchecked status).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DELETE /api/items/:id&lt;/code&gt; deletes an item from the database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The back end supports three configurable properties as well, all of which are exposed as optional environment variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;BACKEND_SERVICE_PORT&lt;/code&gt;, which defaults to &lt;code&gt;8000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BACKEND_ROUTE_PREFIX&lt;/code&gt;, which defaults to &lt;code&gt;/api&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DATABASE_URL&lt;/code&gt;, which defaults to &lt;code&gt;mongodb://127.0.0.1&lt;/code&gt; for local development&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Running the application locally (optional)
&lt;/h2&gt;

&lt;p&gt;You don't have to do this, but if you'd like to, here's how. After &lt;a href="https://docs.mongodb.com/manual/administration/install-community/" rel="noopener noreferrer"&gt;installing MongoDB and starting the service&lt;/a&gt; (which should be listening by default on port &lt;code&gt;27107&lt;/code&gt;), you can install all front-end and back-end dependencies and start the development server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the development server running, you can browse to &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt; and see the app:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F874bcy1v32cj0zpls5r9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F874bcy1v32cj0zpls5r9.png" alt="The application running in a browser on localhost" width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The front-end and back-end dev servers are set up to compile your TypeScript to JavaScript automatically, and the front-end server is configured to proxy the back-end service (which runs at &lt;code&gt;http://localhost:8000&lt;/code&gt;) at a root-relative path of &lt;code&gt;/api&lt;/code&gt;. Proxying the API in this way lets you avoid having to wrestle with &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" rel="noopener noreferrer"&gt;CORS&lt;/a&gt;-related issues, and as you'll see when we deploy to DigitalOcean later, App Platform conveniently supports the same configuration out of the box.&lt;/p&gt;

&lt;p&gt;Try adding a few items and marking them off, just to make sure everything's working as expected. If you've got a MongoDB client installed as well—I generally use &lt;a href="https://www.mongodb.com/products/compass" rel="noopener noreferrer"&gt;MongoDB Compass&lt;/a&gt;—you should be able to find the &lt;code&gt;grocery-list&lt;/code&gt; database and see the &lt;code&gt;items&lt;/code&gt; collection filling up with delicious foods:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi59dnob1yw5tnqzotxdx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi59dnob1yw5tnqzotxdx.png" alt="MongoDB Compass, showing the grocery-list database and the items collection" width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now let's have a look at how to go about deploying this stuff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Charting a course
&lt;/h2&gt;

&lt;p&gt;Earlier I mentioned that every cloud provider handles application deployment a little differently, sometimes in multiple ways, and that's true for DigitalOcean as well. You &lt;em&gt;could&lt;/em&gt; deploy the front end as a &lt;a href="https://www.digitalocean.com/products/spaces" rel="noopener noreferrer"&gt;DigitalOcean Space&lt;/a&gt;, or both the front end and back end (and even the database) as a &lt;a href="https://www.digitalocean.com/products/droplets" rel="noopener noreferrer"&gt;DigitalOcean Droplet&lt;/a&gt;. But given the shape of this application, the best fit is really is &lt;a href="https://docs.digitalocean.com/products/app-platform/concepts/" rel="noopener noreferrer"&gt;App Platform&lt;/a&gt;, for several reasons.&lt;/p&gt;

&lt;p&gt;One is that because App Platform apps are comprised these high-level &lt;a href="https://docs.digitalocean.com/products/app-platform/concepts/" rel="noopener noreferrer"&gt;&lt;em&gt;components&lt;/em&gt;&lt;/a&gt;—abstractions like &lt;a href="https://docs.digitalocean.com/products/app-platform/concepts/static-site/" rel="noopener noreferrer"&gt;static site&lt;/a&gt;, &lt;a href="https://docs.digitalocean.com/products/app-platform/concepts/service/" rel="noopener noreferrer"&gt;service&lt;/a&gt;, and &lt;a href="https://docs.digitalocean.com/products/app-platform/concepts/database/" rel="noopener noreferrer"&gt;database&lt;/a&gt;—it's pretty much purpose-built for an application like this one, and DigitalOcean customizes the deployment of each component based according to its type. Static websites are distributed and cached on DigitalOcean's CDN, services are packaged and delivered as containers (with its &lt;a href="https://www.digitalocean.com/products/kubernetes" rel="noopener noreferrer"&gt;Kubernetes&lt;/a&gt; platform), and databases are deployed as configurable managed services. All of this means you're not only able to stay focused on the application itself, but you're able to scale each one of these components up or down however you like, and even delegate your front-end and back-end build processes to DigitalOcean to be handled in response to commits on one or more external Git repositories.&lt;/p&gt;

&lt;p&gt;App Platform apps can be configured in one of two ways: manually, by configuring their components individually in the DigitalOcean web console, or programmatically, in the form of an App Platform &lt;a href="https://docs.digitalocean.com/products/app-platform/concepts/app-spec/" rel="noopener noreferrer"&gt;&lt;em&gt;spec&lt;/em&gt;&lt;/a&gt;, a JSON document submitted over DigitalOcean's &lt;a href="https://docs.digitalocean.com/reference/api/api-reference/" rel="noopener noreferrer"&gt;REST API&lt;/a&gt;. In our case, we'll indirectly go the latter route, using Pulumi with the &lt;a href="https://www.pulumi.com/registry/packages/digitalocean" rel="noopener noreferrer"&gt;DigitalOcean provider package&lt;/a&gt; to define an app spec comprised of three components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;staticSite&lt;/code&gt; component mapped to the &lt;code&gt;frontend&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;service&lt;/code&gt; component mapped to the &lt;code&gt;backend&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;database&lt;/code&gt; component mapped to a managed MongoDB cluster (which we'll also configure to be accessible only by the &lt;code&gt;service&lt;/code&gt; component)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fewuva73nzehagqtussff.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fewuva73nzehagqtussff.png" alt="Front end, back end, and database tiers mapped to their corresponding App Platform components" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And once deployed, it'll all be available at a single DigitalOcean-provided URL.&lt;/p&gt;

&lt;p&gt;Let's begin by creating new Pulumi project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the project
&lt;/h2&gt;

&lt;p&gt;In the root of the repository, make a new folder called &lt;code&gt;infra&lt;/code&gt;, change to it, then run &lt;code&gt;pulumi new&lt;/code&gt; using the &lt;code&gt;digitalocean&lt;/code&gt; &lt;a href="https://github.com/pulumi/templates" rel="noopener noreferrer"&gt;project template&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;infra &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;infra
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi new digitalocean-typescript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the prompts, use the following values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For project name, use &lt;code&gt;grocery-list&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;For description, use &lt;code&gt;Deploying a MERN-stack app on DigitalOcean&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;For stack name, use &lt;code&gt;dev&lt;/code&gt;, the default&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When the command completes, you'll have a new &lt;a href="https://www.pulumi.com/docs/intro/concepts/stack" rel="noopener noreferrer"&gt;Pulumi stack&lt;/a&gt;, but you'll still have a few things to configure. For one, the Pulumi DigitalOcean provider needs to be &lt;a href="https://www.pulumi.com/registry/packages/digitalocean/installation-configuration" rel="noopener noreferrer"&gt;configured&lt;/a&gt; to communicate with DigitalOcean on your behalf (to provision your app and its various resources). For this, you can use the access token you obtained earlier from the DigitalOcean console, and you can apply it by setting a single environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DIGITALOCEAN_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-access-token"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, since one of our goals is to have App Platform deploy automatically on every GitHub commit, you'll need to tell DigitalOcean where to find the source code for your front- and back-end components. You &lt;em&gt;could&lt;/em&gt; bake these settings right into the Pulumi program itself, but it'd be better to apply them as stack-specific configuration settings, as that'd let you deploy to different stacks later (say, in CI) based on the branch of the commit. Everyone does this a little differently, so for now, let's configure the currently selected stack (which should be &lt;code&gt;dev&lt;/code&gt;) to use the default branch of your GitHub repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;repo &lt;span class="s2"&gt;"your-github-org/your-github-repo"&lt;/span&gt; &lt;span class="c"&gt;# e.g., cnunciato/fullstack-pulumi-mern-digitalocean&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;branch &lt;span class="s2"&gt;"your-main-branch"&lt;/span&gt;               &lt;span class="c"&gt;# e.g., main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these values in place, you're ready to start writing the program.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;App Platform also supports GitLab and other Git-based repositories as well. See the &lt;a href="https://docs.digitalocean.com/products/app-platform/references/app-specification-reference/" rel="noopener noreferrer"&gt;App Specification docs&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Writing the program
&lt;/h2&gt;

&lt;p&gt;In your IDE of choice, open &lt;code&gt;index.ts&lt;/code&gt; and replace the sample code with the following lines to import the Pulumi and DigitalOcean SDKs and the configuration values you just set, and add a line to specify the &lt;a href="https://docs.digitalocean.com/products/platform/availability-matrix/" rel="noopener noreferrer"&gt;DigitalOcean region&lt;/a&gt; to deploy into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/pulumi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;digitalocean&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/digitalocean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Our stack-specific configuration.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;repo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;branch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;branch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The DigitalOcean region to deploy into.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;digitalocean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SFO3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, add a few lines to &lt;a href="https://docs.digitalocean.com/products/databases/mongodb/how-to/create/" rel="noopener noreferrer"&gt;declare the managed MongoDB cluster&lt;/a&gt;. We'll use just one node for now—additional replica nodes can easily be added later by increasing the &lt;code&gt;nodeCount&lt;/code&gt; value—and go with the least expensive &lt;a href="https://www.digitalocean.com/pricing#managed-databases" rel="noopener noreferrer"&gt;performance settings&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="c1"&gt;// Our MongoDB cluster (currently just one node).&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;digitalocean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DatabaseCluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cluster&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mongodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;digitalocean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DatabaseSlug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_1VPCU1GB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;nodeCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// The database we'll use for our grocery list.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;digitalocean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DatabaseDb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grocery-list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;clusterId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now for the App Platform spec itself. Notice the &lt;code&gt;digitalocean.App&lt;/code&gt; resource takes just one argument, &lt;code&gt;spec&lt;/code&gt;, which defines all three of the components of the app: static site, service, and database. Both the static site and the service are configured to use the same GitHub repository (the &lt;code&gt;sourceDir&lt;/code&gt; properties indicate their folders within the repository), and both are configured (via the &lt;code&gt;deployOnPush&lt;/code&gt; flag) to be rebuilt and redeployed by DigitalOcean on every commit.&lt;/p&gt;

&lt;p&gt;The service has a few additional settings that you can use to manage its runtime behavior and deployment topology as well. As in development, we'll configure the service to listen on port 8000 and be available at &lt;code&gt;/api&lt;/code&gt;—the entire app will ultimately be proxied transparently by an &lt;a href="https://docs.digitalocean.com/products/app-platform/concepts/load-balancer/" rel="noopener noreferrer"&gt;App Platform load balancer&lt;/a&gt;—and it'll be powered by just one container instance, again using the least expensive &lt;a href="https://docs.digitalocean.com/products/app-platform/" rel="noopener noreferrer"&gt;performance settings&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="c1"&gt;// The App Platform spec that defines our grocery list.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;digitalocean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grocery-list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="c1"&gt;// The React front end.&lt;/span&gt;
        &lt;span class="na"&gt;staticSites&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;frontend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;deployOnPush&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="na"&gt;sourceDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/frontend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;buildCommand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npm install &amp;amp;&amp;amp; npm run build&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;outputDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/dist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;

        &lt;span class="c1"&gt;// The Express back end.&lt;/span&gt;
        &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;backend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;deployOnPush&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="na"&gt;sourceDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/backend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;buildCommand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npm install &amp;amp;&amp;amp; npm run build&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;npm start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;httpPort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;preservePathPrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="na"&gt;instanceSizeSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;basic-xxs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;instanceCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

                &lt;span class="c1"&gt;// To connect to MongoDB, the service needs a DATABASE_URL, which&lt;/span&gt;
                &lt;span class="c1"&gt;// is conveniently exposed as an environment variable thanks to its&lt;/span&gt;
                &lt;span class="c1"&gt;// membership in this app spec (below). The CA_CERT value enables&lt;/span&gt;
                &lt;span class="c1"&gt;// a secure connection between API service and database.&lt;/span&gt;
                &lt;span class="na"&gt;envs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RUN_AND_BUILD_TIME&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;${db.DATABASE_URL}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CA_CERT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RUN_AND_BUILD_TIME&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;${db.CA_CERT}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;

        &lt;span class="c1"&gt;// Include the MongoDB cluster as an integrated App Platform component.&lt;/span&gt;
        &lt;span class="na"&gt;databases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// The name `db` defines the prefix of the tokens used (above) to&lt;/span&gt;
                &lt;span class="c1"&gt;// read the environment variables exposed by the database cluster.&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

                &lt;span class="c1"&gt;// MongoDB clusters are only available in "production" mode.&lt;/span&gt;
                &lt;span class="c1"&gt;// https://docs.digitalocean.com/products/app-platform/concepts/database/&lt;/span&gt;
                &lt;span class="na"&gt;production&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

                &lt;span class="c1"&gt;// A reference to the `DatabaseCluster` we declared above.&lt;/span&gt;
                &lt;span class="na"&gt;clusterName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cluster&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 class="c1"&gt;// The engine value must be uppercase, so we transform it with JS.&lt;/span&gt;
                &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Technically that's all we need to configure the application—but it wouldn't be a bad idea to add one last thing.&lt;/p&gt;

&lt;p&gt;By default, managed MongoDB clusters are configured to be publicly accessible—which is great if you need to be able to connect one yourself, but not so great as a strategy for preventing internet miscreants from doing the same. You can fix this easily by adding a &lt;code&gt;DatabaseFirewall&lt;/code&gt; resource to declare the app as a &lt;a href="https://docs.digitalocean.com/products/app-platform/how-to/manage-databases/" rel="noopener noreferrer"&gt;&lt;em&gt;trusted source&lt;/em&gt;&lt;/a&gt;, thereby rejecting all inbound traffic originating from elsewhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="c1"&gt;// Adding a database firewall setting grants access solely to our app.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trustedSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;digitalocean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DatabaseFirewall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trusted-source&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;clusterId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, add one last line to export the app URL, to be generated by DigitalOcean, as a Pulumi &lt;a href="https://www.pulumi.com/docs/intro/concepts/inputs-outputs" rel="noopener noreferrer"&gt;stack output&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="c1"&gt;// The DigitalOcean-assigned URL for our app.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;liveUrl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, you're ready to deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying
&lt;/h2&gt;

&lt;p&gt;Quickly, to recap, here's what we've done so far:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We took a pre-baked MERN app configured to run on &lt;code&gt;localhost&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We mapped the tiers of that app to their corresponding App Platform components.&lt;/li&gt;
&lt;li&gt;We wrote a Pulumi program to codify that mapping as an App Platform spec and a managed MongoDB cluster to go along with it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you deploy this app in a moment with &lt;a href="https://www.pulumi.com/docs/reference/cli/pulumi_up" rel="noopener noreferrer"&gt;&lt;code&gt;pulumi up&lt;/code&gt;&lt;/a&gt;, Pulumi will provision a new MongoDB cluster (which usually takes a few minutes), and then once that's available, DigitalOcean will take our spec and use it to fetch the components of the app from GitHub at the specified branch and build them. From that point forward, any commit you make to that branch will trigger DigitalOcean to fetch, rebuild, and redeploy the app automatically.&lt;/p&gt;

&lt;p&gt;Make sure you've installed the DigitalOcean GitHub app as described above—you should see it listed at &lt;a href="https://github.com/settings/installations" rel="noopener noreferrer"&gt;https://github.com/settings/installations&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftk3xwdyruszvb9erx8po.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftk3xwdyruszvb9erx8po.png" alt="The Applications tab on GitHub, showing the DigitalOcean app installed" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now return to the command line and run &lt;code&gt;pulumi up&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up

Previewing update &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;

View Live: https://app.pulumi.com/cnunciato/grocery-list/dev/previews/605bf32a-95b1-4221-bc35-0e667b30f38a

     Type                                    Name              Plan
 +   pulumi:pulumi:Stack                     grocery-list-dev  create
 +   ├─ digitalocean:index:DatabaseCluster   cluster           create
 +   ├─ digitalocean:index:DatabaseDb        db                create
 +   ├─ digitalocean:index:App               app               create
 +   └─ digitalocean:index:DatabaseFirewall  trusted-source    create

Resources:
    + 5 to create

Do you want to perform this update?
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;yes

&lt;/span&gt;Updating &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;

View Live: https://app.pulumi.com/cnunciato/grocery-list/dev/updates/1

     Type                                    Name              Status
     pulumi:pulumi:Stack                     grocery-list-dev
 +   ├─ digitalocean:index:App               app               created
 +   └─ digitalocean:index:DatabaseFirewall  trusted-source    created

Outputs:
  + liveUrl: &lt;span class="s2"&gt;"https://grocery-list-fb9bd.ondigitalocean.app"&lt;/span&gt;

Resources:
    + 2 created
    3 unchanged

Duration: 2m30s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, it'll probably take a few minutes to get everything spun up for the first time, but when the process completes, you'll have a working app at the URL provided by DigitalOcean, emitted as a Pulumi stack output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;open &lt;span class="si"&gt;$(&lt;/span&gt;pulumi stack output liveUrl&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fncqdcjj2075owk3ikj7c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fncqdcjj2075owk3ikj7c.png" alt="The app now running in the DigitalOcean cloud" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should also be able to explore your shiny new grocery-list app in the DigitalOcean Console, with all three of its components (and their build-time and runtime logs!) now represented:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8tbrpi8e8wsoon5my4sd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8tbrpi8e8wsoon5my4sd.png" alt="The grocery-list app and its components in the DigitalOcean console" width="800" height="610"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now try making a commit to your repository (any commit will do, but ideally one to the &lt;code&gt;frontend&lt;/code&gt; or &lt;code&gt;backend&lt;/code&gt; folder), and watch as the app redeploys automatically:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F65zmqhxkp0acigwl2jor.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F65zmqhxkp0acigwl2jor.png" alt="App Platform rebuilding the app in response to a GitHub commit" width="800" height="656"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And finally, try scaling the service by bumping the &lt;code&gt;instanceCount&lt;/code&gt; from &lt;code&gt;1&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt; in the code—or better, if you're up for it, making that value configurable by stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  const config = new pulumi.Config();
  const repo = config.require("repo");
  const branch = config.require("branch");
&lt;span class="gi"&gt;+ const serviceInstanceCount = config.requireNumber("service_instance_count");
&lt;/span&gt;  ...
        services: [
            digitalocean.AppSpecServiceArgs(
                ...
&lt;span class="gi"&gt;+               instanceCount: serviceInstanceCount,
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;service_instance_count 2

&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up

Updating &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;

     Type                       Name              Status      Info
     pulumi:pulumi:Stack        grocery-list-dev
 ~   └─ digitalocean:index:App  app               updated     &lt;span class="o"&gt;[&lt;/span&gt;diff: ~spec]

Outputs:
    liveUrl: &lt;span class="s2"&gt;"https://grocery-list-fb9bd.ondigitalocean.app"&lt;/span&gt;

Resources:
    ~ 1 updated
    4 unchanged

Duration: 1m27s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when you're finished experimenting, you can tear everything down in just a few seconds with a &lt;a href="https://www.pulumi.com/docs/reference/cli/pulumi_destroy" rel="noopener noreferrer"&gt;}}"&amp;gt;&lt;code&gt;pulumi destroy&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi destroy

Destroying &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;

     Type                                    Name              Status
 -   pulumi:pulumi:Stack                     grocery-list-dev  deleted
 -   ├─ digitalocean:index:DatabaseFirewall  trusted-source    deleted
 -   ├─ digitalocean:index:DatabaseDb        db                deleted
 -   ├─ digitalocean:index:App               app               deleted
 -   └─ digitalocean:index:DatabaseCluster   cluster           deleted

Resources:
    - 5 deleted

Duration: 19s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping up, and next steps
&lt;/h2&gt;

&lt;p&gt;Hopefully this gives you a sense of the kinds of things you can do with Pulumi and DigitalOcean—and I definitely encourage you to spend a little time with the &lt;a href="https://docs.digitalocean.com/products/app-platform/" rel="noopener noreferrer"&gt;App Platform docs&lt;/a&gt; to dig a bit deeper into some of these concepts and explore a few others we weren't able to cover. You'll find the &lt;a href="https://github.com/cnunciato/fullstack-pulumi-mern-digitalocean" rel="noopener noreferrer"&gt;full source for this walkthrough on GitHub&lt;/a&gt;, of course, with &lt;a href="https://github.com/cnunciato/fullstack-pulumi-mern-digitalocean/tree/finished" rel="noopener noreferrer"&gt;&lt;code&gt;finished&lt;/code&gt; branch&lt;/a&gt; containing the completed Pulumi program for reference.&lt;/p&gt;

&lt;p&gt;From here, you might think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Adding a &lt;a href="https://www.pulumi.com/registry/packages/digitalocean/api-docs/dnsrecord" rel="noopener noreferrer"&gt;&lt;code&gt;digitalocean.DnsRecord&lt;/code&gt;&lt;/a&gt; to give your app a &lt;a href="https://docs.digitalocean.com/products/networking/dns/" rel="noopener noreferrer"&gt;custom domain name&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Creating a second stack with &lt;a href="https://www.pulumi.com/docs/reference/cli/pulumi_stack_init" rel="noopener noreferrer"&gt;&lt;code&gt;pulumi stack init&lt;/code&gt;&lt;/a&gt; and adjusting the program to make the source &lt;code&gt;branch&lt;/code&gt; configurable—a &lt;code&gt;production&lt;/code&gt; stack, say, designed to deploy in response to commits to a &lt;code&gt;release&lt;/code&gt; branch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Using Pulumi's &lt;a href="https://www.pulumi.com/docs/guides/continuous-delivery/github-actions" rel="noopener noreferrer"&gt;GitHub Action&lt;/a&gt; to run previews and updates as part of a pull-request based workflow.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>pulumi</category>
      <category>fullstack</category>
      <category>mern</category>
      <category>digitalocean</category>
    </item>
    <item>
      <title>Run Your Own RSS Server on AWS with Pulumi</title>
      <dc:creator>Christian Nunciato</dc:creator>
      <pubDate>Tue, 25 Jan 2022 21:27:42 +0000</pubDate>
      <link>https://forem.com/pulumi/run-your-own-rss-server-on-aws-with-pulumi-2n0j</link>
      <guid>https://forem.com/pulumi/run-your-own-rss-server-on-aws-with-pulumi-2n0j</guid>
      <description>&lt;p&gt;It's been quite a few years since &lt;a href="https://googleblog.blogspot.com/2013/03/a-second-spring-of-cleaning.html" rel="noopener noreferrer"&gt;Google shut down Google Reader&lt;/a&gt;, and while a number of nice commercial alternatives have sprung in its wake, none of them has ever been quite the right fit for me personally.&lt;/p&gt;

&lt;p&gt;So a while back, after far too much time spent wandering the blogsphere manually, typing URLs into address bars by hand, I decided to go looking to see whether the universe had produced an open-source solution to this problem --- and to my surprise and delight, it had! &lt;a href="https://miniflux.app/" rel="noopener noreferrer"&gt;Miniflux&lt;/a&gt; is an excellent little open-source RSS server and reader, written in Go and backed by PostgreSQL, that also happens to be packaged &lt;a href="https://hub.docker.com/r/miniflux/miniflux" rel="noopener noreferrer"&gt;as a Docker container&lt;/a&gt;. So in this post, I'll show how easy it is to deploy a Miniflux server of your own on AWS, using only Pulumi and a few lines of TypeScript.&lt;/p&gt;

&lt;p&gt;If you're already comfortable with Pulumi, and you just want to get up and running, &lt;a href="https://github.com/pulumi/examples/tree/master/aws-ts-pulumi-miniflux" rel="noopener noreferrer"&gt;I've set up a GitHub repo&lt;/a&gt; (complete with a &lt;a href="https://pulumi.com/docs/intro/pulumi-service/pulumi-button" rel="noopener noreferrer"&gt;Deploy with Pulumi button&lt;/a&gt;!) that should have all you need to get going. Just click the button, set a few configs (like your RSS server's administrative password, which will be stored as an &lt;a href="https://www.pulumi.com/docs/intro/concepts/secrets/" rel="noopener noreferrer"&gt;encrypted Pulumi secret&lt;/a&gt;), and follow the prompts. Your shiny new server should be up and running within minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sketching it Out
&lt;/h2&gt;

&lt;p&gt;First, let's have a look at the app we'll be building. It'll consist mainly of two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;Miniflux service&lt;/strong&gt;, which runs as a web application and API service, and&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Miniflux database&lt;/strong&gt;, a PostgreSQL instance whose schema and data are fully managed by the Miniflux service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As I mentioned, the service is packaged as a Docker container that runs on port 8080 by default, and exposes a number of environment variables we can use to configure it, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DATABASE_URL&lt;/code&gt;, a PostgreSQL connection string used by the service to communicate with the PostgreSQL database,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ADMIN_USERNAME&lt;/code&gt; and &lt;code&gt;ADMIN_PASSWORD&lt;/code&gt;, which we'll use to sign into the service for the first time, and&lt;/li&gt;
&lt;li&gt;flags for &lt;code&gt;CREATE_ADMIN&lt;/code&gt; and &lt;code&gt;RUN_MIGRATIONS&lt;/code&gt;, which denote whether to create the administrative user and run database migrations on startup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll set these properties in the course of developing our program. The service itself, as a container, can easily be run on &lt;a href="https://aws.amazon.com/fargate/" rel="noopener noreferrer"&gt;AWS Fargate&lt;/a&gt;, so we can use the &lt;a href="https://pulumi.com/docs/reference/pkg/nodejs/pulumi/awsx" rel="noopener noreferrer"&gt;&lt;code&gt;@pulumi/awsx&lt;/code&gt;&lt;/a&gt; package to declare it, and for the database, we'll use &lt;a href="https://pulumi.com/registry/packages/aws/api-docs" rel="noopener noreferrer"&gt;&lt;code&gt;@pulumi/aws&lt;/code&gt;&lt;/a&gt; to provision a small &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_PostgreSQL.html" rel="noopener noreferrer"&gt;RDS instance of PostgreSQL&lt;/a&gt;. And finally, to make the Miniflux service publicly accessible (initially over HTTP; we'll switch to HTTPS later), we'll use an AWS &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/network/network-load-balancers.html" rel="noopener noreferrer"&gt;Network Load Balancer&lt;/a&gt;, since we'll only need to be able to route traffic by ports and protocols.&lt;/p&gt;

&lt;p&gt;Our architecture is therefore shaping up to look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7vbfy504edwinltl1ayk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7vbfy504edwinltl1ayk.png" alt="An architecture diagram showing a load balancer, Miniflux service, and database" width="800" height="111"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up
&lt;/h2&gt;

&lt;p&gt;We'll start by creating a new Pulumi project. (If you're completely new to Pulumi, it might be good to begin with our &lt;a href="https://pulumi.com/docs/get-started" rel="noopener noreferrer"&gt;Getting Started guide&lt;/a&gt;, which walks you through installing Pulumi and configuring it for your cloud provider.) We'll use the built-in &lt;code&gt;aws-typescript&lt;/code&gt; project template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;miniflux &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;miniflux
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi new aws-typescript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you've stepped through the prompts, you should be left with a minimal &lt;code&gt;index.ts&lt;/code&gt; file that defines and exports an S3 bucket --- but since we won't be needing an S3 bucket for this project, we can remove that code, and keep only the imports we'll need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/pulumi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/aws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;awsx&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/awsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's define a few configuration values for the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the Stack
&lt;/h2&gt;

&lt;p&gt;While we could certainly hard-code all of these values into our program, it'd be better to use Pulumi to set them, since doing so give us the option to vary them by &lt;a href="https://pulumi.com/docs/intro/concepts/stack" rel="noopener noreferrer"&gt;stack&lt;/a&gt; (say, if we wanted to run this particular app in multiple environments), but more importantly, to set some passwords for the database user and service administrator. So let's do that first, so we'll have them all ready as we develop our program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;db_name miniflux
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;db_username miniflux
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;db_password somesupersecretpassword &lt;span class="nt"&gt;--secret&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;admin_username admin
&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi config &lt;span class="nb"&gt;set &lt;/span&gt;admin_password anothersupersecretpassword &lt;span class="nt"&gt;--secret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can change any of these values if you like (and you should definitely change the two passwords, of course), but note that the passwords are set with the &lt;code&gt;--secret&lt;/code&gt; flag, which encrypts their values using Pulumi's &lt;a href="https://www.pulumi.com/docs/intro/concepts/secrets/" rel="noopener noreferrer"&gt;built-in support for secrets&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Okay. With our configuration set, we're ready to start building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the Code
&lt;/h2&gt;

&lt;p&gt;The Pulumi program we're writing will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;import our newly created Pulumi configuration, so we can use its values in our program;&lt;/li&gt;
&lt;li&gt;create a new &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.WorkingWithRDSInstanceinaVPC.html" rel="noopener noreferrer"&gt;DB Subnet Group&lt;/a&gt; using the default &lt;a href="https://docs.aws.amazon.com/vpc/index.html" rel="noopener noreferrer"&gt;VPC&lt;/a&gt; for your region;&lt;/li&gt;
&lt;li&gt;create a new instance of PostgreSQL, with minimal settings, placing it into the subnet group and giving it access to your default &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_clusters.html" rel="noopener noreferrer"&gt;ECS Cluster&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;create a new &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-listeners.html" rel="noopener noreferrer"&gt;Network Listener&lt;/a&gt; to define a publicly accessible URL for the Miniflux service;&lt;/li&gt;
&lt;li&gt;create a new Fargate service for the Miniflux app, in your default ECS Cluster, passing it the newly created DB connection and Pulumi config settings; and finally,&lt;/li&gt;
&lt;li&gt;export the URL as a Pulumi stack &lt;a href="https://pulumi.com/docs/intro/concepts/inputs-outputs" rel="noopener noreferrer"&gt;Output&lt;/a&gt; so we can navigate to the service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's get coding!&lt;/p&gt;

&lt;p&gt;Replace the contents of &lt;code&gt;index.ts&lt;/code&gt; with the following code, which comprises the whole program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/pulumi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/aws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;awsx&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/awsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Import our Pulumi configuration.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dbName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;db_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dbUsername&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;db_username&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dbPassword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;db_password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adminUsername&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin_username&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adminPassword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin_password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Get the default VPC and ECS Cluster for your account.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;awsx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;awsx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Create a new subnet group for the database.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subnetGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;rds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SubnetGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dbsubnets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;subnetIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicSubnetIds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Create a new database, using the subnet and cluster groups.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;rds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgres&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;instanceClass&lt;/span&gt;&lt;span class="p"&gt;:&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;rds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InstanceTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;T3_Micro&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;allocatedStorage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;dbSubnetGroupName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;subnetGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;vpcSecurityGroupIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;securityGroups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dbName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dbUsername&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dbPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;skipFinalSnapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Assemble a connection string for the Miniflux service.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connectionString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interpolate&lt;/span&gt; &lt;span class="s2"&gt;`postgres://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dbUsername&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dbPassword&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/miniflux?sslmode=disable`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create an NetworkListener to forward HTTP traffic on port 8080.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;awsx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NetworkListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Create a Fargate service consisting of just one container instance (since that's all we&lt;/span&gt;
&lt;span class="c1"&gt;// really need), passing it the cluster, DB connection and Pulumi config settings.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;awsx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FargateService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;service&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;desiredCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;taskDefinitionArgs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;miniflux/miniflux:latest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;portMappings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="nx"&gt;listener&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;connectionString&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RUN_MIGRATIONS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CREATE_ADMIN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ADMIN_USERNAME&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adminUsername&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ADMIN_PASSWORD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adminPassword&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Export the publicly accessible URL.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interpolate&lt;/span&gt; &lt;span class="s2"&gt;`http://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the program in place, we can bring it to life with &lt;code&gt;pulumi up&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a few minutes, you should see that Pulumi has created 32 new resources:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1we70oz1fyexqnm4yx0y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1we70oz1fyexqnm4yx0y.png" alt="A screenshot of the results of the first Pulumi update" width="800" height="781"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you examine the logs for this stack using &lt;code&gt;pulumi logs -f&lt;/code&gt;, you'll see that the database was created, migrations were run, administrative user was created, and the service is now listening:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F12pxjdxmmct477410kvt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F12pxjdxmmct477410kvt.png" alt="A screenshot showing the output of the Miniflux service, now listening on port 8080" width="800" height="549"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, looking back at the output of &lt;code&gt;pulumi up&lt;/code&gt;, you'll see the URL we exported, to which you can navigate, sign in with the administrative user created by our program, and start RSSing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Filanxcv3l9dlba217imi.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Filanxcv3l9dlba217imi.gif" alt="An animation showing the process of logging into the Miniflux console in a web browser" width="942" height="655"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Excellent! You've now got your very own fully functioning RSS service, created with only a few dozen lines of code.&lt;/p&gt;

&lt;p&gt;Technically, we're done --- but there are a couple of things we might like to do to make this project a bit more complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running on Port 80
&lt;/h2&gt;

&lt;p&gt;Right now, our service's load balancer is configured to listen and forward on the same port, 8080. If we wanted to have the load balancer listen on port 80 (or some other port) instead, we'd need to change its declaration a bit, to create the load balancer and &lt;a href="https://docs.aws.amazon.com/en_pv/elasticloadbalancing/latest/network/load-balancer-target-groups.html" rel="noopener noreferrer"&gt;target group&lt;/a&gt; more explicitly. Let's do that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create a load balancer, target group and network listener explicitly.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;awsx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NetworkLoadBalancer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTargetGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;group&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;listener&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;pulumi up&lt;/code&gt;, and you'll see that the service is now accessible at a new host and port (though the database was of course left unchanged):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3bg996ozbw4q40c935wq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3bg996ozbw4q40c935wq.png" alt="A screenshot of the Pulumi update showing the load balancer port number changing from 8080 to 80" width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Great! But what if we wanted to run over HTTPS, instead?&lt;/p&gt;

&lt;h2&gt;
  
  
  Running over HTTPS
&lt;/h2&gt;

&lt;p&gt;In order to run the Miniflux service over HTTPS, you'll need to have obtained an SSL certificate from &lt;a href="https://docs.aws.amazon.com/acm/latest/userguide/acm-overview.html" rel="noopener noreferrer"&gt;AWS Certificate Manager&lt;/a&gt; that corresponds with the domain you plan to use for the service.&lt;/p&gt;

&lt;p&gt;Provided you've obtained that certificate, it's defined in the same AWS region as the one you've configured for your Pulumi stack, and you're able to make changes to the DNS records for the domain associated with the certificate, then updating the Pulumi program is easy --- just change the &lt;code&gt;listener&lt;/code&gt; declaration to use TLS and port 443, and add a &lt;code&gt;certificateArn&lt;/code&gt; property to apply the certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Run the service over HTTPS, terminating SSL at the load balancer and forwarding to port 8080 on the container.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;awsx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NetworkLoadBalancer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTargetGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;group&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TCP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;listener&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TLS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;certificateArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;arn:aws:acm:...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;pulumi up&lt;/code&gt; one last time, and you should see your service now running on port 443 --- but you won't be able to access it until you add CNAME record to the DNS settings for your domain (I happen to use Google for mine) mapping a new subdomain to your load balancer's hostname:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmkyh1ntle803258utxne.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmkyh1ntle803258utxne.png" alt="A screenshot showing how to add a CNAME for the Miniflux service to a domain managed with Google domains" width="800" height="148"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And with that, you should now be able to browse your RSS server securely:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffjkhmbctutndlow022se.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffjkhmbctutndlow022se.png" alt="A screenshot showing the Miniflux console running under HTTPS with a custom domain" width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Finishing Up
&lt;/h2&gt;

&lt;p&gt;In this post, we've seen how easy it is to run a container as a service connected to an RDS database with Pulumi, and to expose that container securely on the web. If we wanted, we could go even farther --- we could refactor the program into Pulumi &lt;a href="https://pulumi.com/docs/intro/concepts/resources#components" rel="noopener noreferrer"&gt;Components&lt;/a&gt;, perhaps (one for the service, one for the database), package it up for sharing on npm, and so on.&lt;/p&gt;

&lt;p&gt;But we'll leave those improvements for another day. For now, let's enjoy what we've created! And start catching up on all that reading we've missed.&lt;/p&gt;

</description>
      <category>pulumi</category>
      <category>javascript</category>
      <category>rss</category>
      <category>miniflux</category>
    </item>
    <item>
      <title>API Gateway to EventBridge with Pulumi</title>
      <dc:creator>Christian Nunciato</dc:creator>
      <pubDate>Thu, 20 Jan 2022 05:28:17 +0000</pubDate>
      <link>https://forem.com/pulumi/api-gateway-to-eventbridge-with-pulumi-1de4</link>
      <guid>https://forem.com/pulumi/api-gateway-to-eventbridge-with-pulumi-1de4</guid>
      <description>&lt;p&gt;If you're familiar with &lt;a href="https://aws.amazon.com/api-gateway/" rel="noopener noreferrer"&gt;Amazon API Gateway&lt;/a&gt;, you know it's all about making it easier to provision and manage a web API. Maybe you've used it, as I have, with &lt;a href="https://pulumi.com/docs/guides/crosswalk/aws" rel="noopener noreferrer"&gt;Crosswalk&lt;/a&gt;, our AWS extension library, to stand up a REST API and handle requests with AWS Lambda functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;awsx&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/awsx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create a new API Gateway instance.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;awsx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apigateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;API&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Define an HTTP endpoint.&lt;/span&gt;
            &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/things&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

            &lt;span class="c1"&gt;// Handle requests with an AWS Lambda function.&lt;/span&gt;
            &lt;span class="na"&gt;eventHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiGatewayEvent&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 class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;thingOne&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;thingTwo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="p"&gt;};&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Export the API's public URL. 🎉&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I love this abstraction, and I use it all the time; it's an incredibly convenient way to solve a really common problem. But if you examine the code, you'll notice that it's making a fairly strong assumption about how you'll be handling HTTP requests — namely, that you'll do so with a single Lambda function, and that that function will always return a JavaScript object &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-integration-settings-integration-response.html" rel="noopener noreferrer"&gt;of a particular shape&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Indeed, this arrangement is the API contract of a &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html" rel="noopener noreferrer"&gt;Lambda &lt;em&gt;proxy&lt;/em&gt; integration&lt;/a&gt; — API Gateway integrations come in &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations.html" rel="noopener noreferrer"&gt;many shapes and sizes&lt;/a&gt;; Lambda integrations just happen to be one of the more popular — and much of the time, an approach like this one works out just fine. But depending on the needs of the application, it might not always be the best fit.&lt;/p&gt;

&lt;p&gt;Imagine you were building a print-on-demand service, for example, and you wanted to expose an API to let your customers upload  documents and have them converted into orders. On AWS, you might reach for API Gateway, as above, to define an HTTP method and route (&lt;code&gt;POST /uploads&lt;/code&gt;, say), wire it up to an AWS Lambda, and have the Lambda parse the upload, write the order to a database, and return a response. Visually, such a design might look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9o7j9qn8m3ysdpcxeuht.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9o7j9qn8m3ysdpcxeuht.png" alt="A diagram showing an HTTP POST made to an API Gateway endpoint, the endpoint invoking a Lambda function, and the Lambda function writing an order to a database." width="800" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It'd definitely work, and again, it's quite common. But at some point, you might find this tight coupling between API Gateway and Lambda too limiting. Say you wanted to be notified whenever a new order was received, with a Slack message, maybe, in one of your team's shared workspace channels. Under the current design, you'd probably add a few lines of code to the Lambda function to import an HTTP library and make a call to the Slack API to post the message:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8lsepotagu5byg0wctbu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8lsepotagu5byg0wctbu.png" alt="The same diagram showing the same Lambda function extended to post a message to Slack." width="800" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That'd work, too — but it'd be less than ideal for a number of reasons. For one, that Lambda would now have two jobs: taking orders and sending Slack notifications. That might be fine for today (it’s only a few lines of code, after all), but over time, those two jobs could easily become three, and then four, and soon enough, that poor Lambda could wind up becoming a lot more difficult to maintain. And given the importance of its main job — capturing orders — it's not something you'd want to risk failing at runtime because of a random Slack outage or other transient internet mishap. Moreover, with every bit of extra work you tack on to that function, you take a tiny step closer to hitting API Gateway's &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html" rel="noopener noreferrer"&gt;30-second limit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What you really need, then, is to be able to take multiple independent, possibly long-running actions based on a single API Gateway request. And one way to do that is with &lt;a href="https://aws.amazon.com/eventbridge" rel="noopener noreferrer"&gt;Amazon EventBridge&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hello, EventBridge
&lt;/h2&gt;

&lt;p&gt;Amazon EventBridge (formerly CloudWatch Events) is as a serverless &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-bus.html" rel="noopener noreferrer"&gt;&lt;em&gt;event bus&lt;/em&gt;&lt;/a&gt; whose job is to receive structured &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-events.html" rel="noopener noreferrer"&gt;&lt;em&gt;event data&lt;/em&gt;&lt;/a&gt; — from your own applications, from other AWS Services — and use that data to notify other applications or services using event-handling &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rules.html" rel="noopener noreferrer"&gt;&lt;em&gt;rules&lt;/em&gt;&lt;/a&gt; that you specify. With EventBridge, you can build loosely coupled, event-driven systems that take advantage of AWS's rich support for serverless architectures and that scale gracefully as the needs of those systems change over time.&lt;/p&gt;

&lt;p&gt;For this particular application, EventBridge would let you address the multiple-handler problem in a more scalable and easily maintainable way. Rather than have API Gateway invoke Lambda directly, leaving one Lambda responsible for handling multiple tasks, you could have API Gateway publish to EventBridge instead, and let EventBridge invoke as many Lambdas (or other AWS services) as you like, all serverlessly and in parallel — and of course, all easily managed with Pulumi.&lt;/p&gt;

&lt;p&gt;Let's see how. Here's a revised architecture diagram showing how you might approach building an application like this one with EventBridge positioned between API Gateway and Lambda:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdrauq93e53j5pq0x3mo3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdrauq93e53j5pq0x3mo3.png" alt="An expanded diagram showing API Gateway now publishing to EventBridge, with EventBridge invoking two separate Lambda functions: one writing an order to a database, the other posting a message to Slack." width="800" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now let's have a look at what it'd be like to build it with Pulumi. We won't build &lt;em&gt;everything&lt;/em&gt; in this diagram — things like writing to the database or messaging Slack are left for you to explore — but we will build enough to give you clear picture and a working example of how to connect all of these parts into a working application. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an API Gateway &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html" rel="noopener noreferrer"&gt;instance&lt;/a&gt; to act as a container for your public API, along with a &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-stages.html" rel="noopener noreferrer"&gt;&lt;em&gt;stage&lt;/em&gt;&lt;/a&gt; and a &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-routes.html" rel="noopener noreferrer"&gt;&lt;em&gt;route&lt;/em&gt;&lt;/a&gt; to handle inbound HTTP requests;&lt;/li&gt;
&lt;li&gt;an EventBridge &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations.html" rel="noopener noreferrer"&gt;integration&lt;/a&gt; (comprised of an event bus and event rule) to handle notifications from API Gateway; and finally,&lt;/li&gt;
&lt;li&gt;one or more Lambda functions to be invoked in response to event-rule matches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a new project and stack
&lt;/h2&gt;

&lt;p&gt;As always, it’s good practice to begin with a new project and stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi new aws-typescript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure you &lt;a href="https://pulumi.com/registry/packages/aws/installation-configuration" rel="noopener noreferrer"&gt;configure your AWS credentials&lt;/a&gt; as well, and when prompted, choose whatever stack name and AWS region work best for you.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In this post, we'll be using &lt;a href="https://docs.aws.amazon.com/apigatewayv2/latest/api-reference/api-reference.html" rel="noopener noreferrer"&gt;API Gateway V2&lt;/a&gt; as it's newer and has a few features (like &lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-stage.html#aws-resource-apigatewayv2-stage-properties" rel="noopener noreferrer"&gt;auto-deploy&lt;/a&gt;) that make it easier to work with in this context. The same ideas apply to both V1 and V2, though, so while V2 still lacks full feature parity with V1, the underlying resources we'll be using — methods, routes, integrations — are the same. You'll find links to examples of both at the end of this post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Create the API gateway and stage
&lt;/h2&gt;

&lt;p&gt;Start by replacing the contents of &lt;code&gt;index.ts&lt;/code&gt; with the following code to declare a new API Gateway &lt;a href="https://pulumi.com/registry/packages/aws/api-docs/apigatewayv2/api" rel="noopener noreferrer"&gt;API&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/pulumi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/aws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create an HTTP API.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;apigatewayv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;protocolType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HTTP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, add a stage (feel free to name it whatever you like; I usually use the current stack name for convenience), and set it to deploy automatically whenever a change is made to the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="c1"&gt;// Create a stage and set it to deploy automatically.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;apigatewayv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Stage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;apiId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStack&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;autoDeploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next thing to do is register a route on the gateway to give your users a publicly accessible endpoint to upload to. But in order to do &lt;em&gt;that&lt;/em&gt;, you'll need to tell API Gateway what to do when an upload happens. Since the plan of record is to notify EventBridge (using API Gateway’s built-in support for it), you’ll need to declare a few EventBridge things first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add an event bus and event rule
&lt;/h2&gt;

&lt;p&gt;Every AWS account gets an event bus by default (one that's aptly named &lt;code&gt;default&lt;/code&gt;), but given how easy it is to create one, we might as well do that for this application. You'll also need to define an event rule — a resource that watches a specific event bus for events that conform to a particular pattern or shape, and then routes those events to one or more targets (e.g., Lambda functions). Add the following lines to your program for both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="c1"&gt;// Create an event bus.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;cloudwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventBus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Create an event rule to watch for events.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;cloudwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rule&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;eventBusName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bus&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 class="c1"&gt;// Specify the event pattern to watch for.&lt;/span&gt;
    &lt;span class="na"&gt;eventPattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-event-source&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most noteworthy property of the &lt;code&gt;EventRule&lt;/code&gt; resource is probably the &lt;code&gt;eventPattern&lt;/code&gt;. EventBridge events all conform to a certain schema, and in this case, we’re expressing that this particular event rule should take action on any event that originates from &lt;code&gt;my-event-source&lt;/code&gt;. (The &lt;code&gt;source&lt;/code&gt; property is just a free-form string that by convention identifies the application or service responsible for the event.)&lt;/p&gt;

&lt;p&gt;With the event bus and event rule in place, you’re ready to define the integration itself — the resource responsible for connecting the gateway route (which we’ll get back to shortly) to your newly created event bus. As I mentioned earlier, there are several types of API Gateway integration to choose from, each one suited to a particular purpose. For this example, the &lt;code&gt;AWS_PROXY&lt;/code&gt; type is good fit, as it’s simple and requires very little code; it doesn't give you quite as much control over the API Gateway response as you might like — as a proxy, it just returns to the caller whatever is returned by the backend, in this case EventBridge — but it's more than enough for the task at hand.&lt;/p&gt;

&lt;p&gt;Add the following lines for the integration and route. The comments should explain what each block is doing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="c1"&gt;// Define a policy granting API Gateway permission to publish to EventBridge.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiGatewayRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api-gateway-role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;assumeRolePolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2012-10-17&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sts:AssumeRole&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;apigateway.amazonaws.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;managedPolicyArns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;arn:aws:iam::aws:policy/AmazonEventBridgeFullAccess&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Create an API Gateway integration to forward requests to EventBridge.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;integration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;apigatewayv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Integration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;integration&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;apiId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// The integration type and subtype.&lt;/span&gt;
    &lt;span class="na"&gt;integrationType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AWS_PROXY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;integrationSubtype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EventBridge-PutEvents&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;credentialsArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiGatewayRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="c1"&gt;// The body of the request to be sent to EventBridge. Note the&lt;/span&gt;
    &lt;span class="c1"&gt;// event source matches pattern defined on the EventRule, and the&lt;/span&gt;
    &lt;span class="c1"&gt;// Detail expression, which just forwards the body of the original&lt;/span&gt;
    &lt;span class="c1"&gt;// API Gateway request (i.e., the uploaded document).&lt;/span&gt;
    &lt;span class="na"&gt;requestParameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;EventBusName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bus&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 class="na"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-event-source&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;DetailType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-detail-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$request.body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Finally, define the route.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;apigatewayv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;route&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;apiId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;routeKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST /uploads&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interpolate&lt;/span&gt;&lt;span class="s2"&gt;`integrations/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;integration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, you’re ready to configure the Lambda.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a Lambda function handler
&lt;/h2&gt;

&lt;p&gt;The hard part’s done: you’ve declared an API and route, mapped that route to an integration, configured the integration to publish events to an event bus, and defined an event rule to respond to those events. All that's left now is to tell the rule &lt;em&gt;how&lt;/em&gt; to respond.&lt;/p&gt;

&lt;p&gt;So to finish things off, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Lambda function to handle uploads,&lt;/li&gt;
&lt;li&gt;an EventBridge &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-targets.html" rel="noopener noreferrer"&gt;&lt;em&gt;target&lt;/em&gt;&lt;/a&gt; to bind your event rule to that function, and&lt;/li&gt;
&lt;li&gt;an IAM policy granting EventBridge permission to invoke the function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add the following lines to your program to complete it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="c1"&gt;// Create a Lambda function handler with permission to log to CloudWatch.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CallbackFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lambda&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&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;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ManagedPolicies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CloudWatchLogsFullAccess&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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 class="c1"&gt;// For now, just log the event, including the uploaded document.&lt;/span&gt;
        &lt;span class="c1"&gt;// That'll be enough to verify everything's working.&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Create an EventBridge target associating the event rule with the function.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lambdaTarget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;cloudwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lambda-target&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rule&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 class="na"&gt;eventBusName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bus&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 class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Give EventBridge permission to invoke the function.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lambdaPermission&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Permission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lambda-permission&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lambda:InvokeFunction&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;events.amazonaws.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Export the API Gateway URL to give us something to POST to.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interpolate&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiEndpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;stage&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 class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  All Together, Now
&lt;/h2&gt;

&lt;p&gt;Now that the program's complete, you can run Pulumi to bring it to life:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up
...

Updating &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;
...

     Type                             Name                      Status
 +   pulumi:pulumi:Stack              eventbridge-v2-dev        created
 +   ├─ aws:apigatewayv2:Api          api                       created
 +   ├─ aws:apigatewayv2:Stage        stage                     created
 +   ├─ aws:cloudwatch:EventBus       bus                       created
 ...

Outputs:
    apiURL: &lt;span class="s2"&gt;"https://geqfietbcl.execute-api.us-west-2.amazonaws.com/dev"&lt;/span&gt;

Resources:
    + 15 created

Duration: 31s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the update finishes, you'll have a fully functioning API Gateway-EventBridge integration that you can verify with &lt;code&gt;curl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{"some-key": "some-value"}'&lt;/span&gt; &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pulumi stack output url&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/uploads"&lt;/span&gt;

&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"Entries"&lt;/span&gt;:[&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"EventId"&lt;/span&gt;:&lt;span class="s2"&gt;"cdc44763-6976-286c-9378-7cce674dff81"&lt;/span&gt;&lt;span class="o"&gt;}]&lt;/span&gt;,&lt;span class="s2"&gt;"FailedEntryCount"&lt;/span&gt;:0&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the response, which comes directly from EventBridge (courtesy of the &lt;code&gt;AWS_PROXY&lt;/code&gt; integration), confirming the event was received and written to the event bus.&lt;/p&gt;

&lt;p&gt;The final step is to confirm that the request made it all the way to Lambda, which you can easily do by tailing its output with &lt;code&gt;pulumi logs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi logs &lt;span class="nt"&gt;--follow&lt;/span&gt;

Collecting logs &lt;span class="k"&gt;for &lt;/span&gt;stack dev since 2022-01-06T16:18:48.000-08:00.
...

&lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;source&lt;/span&gt;: &lt;span class="s1"&gt;'my-event-source'&lt;/span&gt;,
    detail: &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="s1"&gt;'some-key'&lt;/span&gt;: &lt;span class="s1"&gt;'some-value'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you're happy, be sure to tidy up with a &lt;code&gt;pulumi destroy&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;There's a lot more you can do with integrations like this that we didn't cover: add more Lambda function handlers, have EventBridge target &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-targets.html" rel="noopener noreferrer"&gt;other AWS services&lt;/a&gt; (&lt;a href="https://aws.amazon.com/step-functions" rel="noopener noreferrer"&gt;Step Functions&lt;/a&gt; might be a good one to try next), validate HTTP request bodies (with API Gateway &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/models-mappings.html" rel="noopener noreferrer"&gt;models&lt;/a&gt;, to keep bad data from ever reaching EventBridge), and more. Hopefully this gives you sense of what's possible, though. And as promised, you'll find examples that use both versions of API Gateway in our &lt;a href="https://github.com/pulumi/examples" rel="noopener noreferrer"&gt;examples repository on GitHub&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/pulumi/examples/tree/master/aws-ts-apigatewayv2-eventbridge" rel="noopener noreferrer"&gt;API Gateway V2 to EventBridge&lt;/a&gt; in TypeScript&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/pulumi/examples/tree/master/aws-py-apigatewayv2-eventbridge" rel="noopener noreferrer"&gt;API Gateway V2 to EventBridge&lt;/a&gt; in Python&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/pulumi/examples/tree/master/aws-ts-apigateway-eventbridge" rel="noopener noreferrer"&gt;API Gateway V1 to EventBridge&lt;/a&gt; in TypeScript, with request validation and custom HTTP response mapping&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>pulumi</category>
      <category>aws</category>
      <category>javascript</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Imperatively Declarative: How (and why) Pulumi is different</title>
      <dc:creator>Christian Nunciato</dc:creator>
      <pubDate>Sun, 17 Oct 2021 20:44:49 +0000</pubDate>
      <link>https://forem.com/cnunciato/imperatively-declarative-how-and-why-pulumi-is-different-3bfb</link>
      <guid>https://forem.com/cnunciato/imperatively-declarative-how-and-why-pulumi-is-different-3bfb</guid>
      <description>&lt;p&gt;In conversations about &lt;a href="https://en.wikipedia.org/wiki/Infrastructure_as_code" rel="noopener noreferrer"&gt;infrastructure as code&lt;/a&gt;, the debate over &lt;a href="https://en.wikipedia.org/wiki/Infrastructure_as_code#Types_of_approaches" rel="noopener noreferrer"&gt;imperative versus declarative&lt;/a&gt; tools still comes up from time to time. Actually, there's not much left to debate: declarative's pretty much won. But somehow, the subject still manages to get people going, probably because what "declarative" means isn't quite as clear as it used to be --- and that's partly because of tools like Pulumi.&lt;/p&gt;

&lt;p&gt;When Pulumi comes up in one of these conversations, it usually gets placed on the imperative end of the spectrum; it's an easy mistake to make, considering Pulumi programs are written in imperative languages like JavaScript. But it's a mistake nonetheless. &lt;a href="https://twitter.com/brianleroux/status/1415310789167046657" rel="noopener noreferrer"&gt;Here's an example of such an exchange&lt;/a&gt; from a couple of weeks ago, for instance:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I will be very specific using my previous example. JS is imperative. JSON is declarative.&lt;/p&gt;

&lt;p&gt;--- Brian LeRoux (&lt;a class="mentioned-user" href="https://dev.to/brianleroux"&gt;@brianleroux&lt;/a&gt;) &lt;a href="https://twitter.com/brianleroux/status/1415310789167046657" rel="noopener noreferrer"&gt;July 14, 2021&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's worth mentioning that Brian is the creator of &lt;a href="https://arc.codes/docs/en/guides/get-started/quickstart" rel="noopener noreferrer"&gt;arc.codes&lt;/a&gt;, a command-line tool that lets you write blocks of JSON or YAML to deploy serverless functions and other things on &lt;a href="https://aws.amazon.com/" rel="noopener noreferrer"&gt;AWS&lt;/a&gt;. Arc is a perfect example of simple, declarative infrastructure as code that's focused on making the &lt;a href="https://www.google.com/books/edition/Programming_Perl/xx5JBSqcQzIC?hl=en&amp;amp;gbpv=1&amp;amp;bsq=%22easy%20things%20should%20be%20easy,%20and%20hard%20things%20should%20be%20possible%22" rel="noopener noreferrer"&gt;easy things easy&lt;/a&gt;. Take a look at this terse little Arc file, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello-world"&lt;/span&gt;
&lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/thing1"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/thing2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Arc, this bit of YAML states that at the end of an Arc run, two publicly accessible HTTP endpoints should exist in &lt;a href="https://aws.amazon.com" rel="noopener noreferrer"&gt;AWS Lambda&lt;/a&gt; (at a URL dynamically assigned by AWS) at the paths &lt;code&gt;/thing1&lt;/code&gt; and &lt;code&gt;/thing2&lt;/code&gt;, and that both endpoints should be wired up to respond to HTTP &lt;code&gt;GET&lt;/code&gt;s. When you run this file with the Arc CLI --- assuming you've stashed your AWS credentials in the right place, and put your JavaScript functions in a nearby subfolder --- that'll indeed be the case: a minute or so later, those endpoints &lt;em&gt;will&lt;/em&gt; exist, and all will be right with the world. Easy.&lt;/p&gt;

&lt;p&gt;Moreover, if you were to run that code a &lt;em&gt;second&lt;/em&gt; time (having made no changes to the YAML or JavaScript), nothing would happen, because the "desired state" you'd expressed in the &lt;code&gt;arc.yaml&lt;/code&gt; file would already have been achieved: with those two endpoints deployed and running in the AWS cloud, Arc (by way of &lt;a href="https://aws.amazon.com/cloudformation/" rel="noopener noreferrer"&gt;CloudFormation&lt;/a&gt;) would have nothing more to do for you. That's &lt;a href="https://en.wikipedia.org/wiki/Declarative_programming" rel="noopener noreferrer"&gt;declarative&lt;/a&gt; infrastructure-as-code (IaC) at work: you describe &lt;em&gt;what you want&lt;/em&gt; --- two HTTP endpoints --- and the IaC tool determines the &lt;em&gt;how&lt;/em&gt;, computing the work to be done and then making it happen for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Imperative_programming" rel="noopener noreferrer"&gt;&lt;em&gt;Imperative&lt;/em&gt;&lt;/a&gt; IaC, on the other hand, is different. In imperative programming (e.g., in most JavaScript), the code that you write is all about control --- &lt;em&gt;do this, then that; if this, then that&lt;/em&gt;. A good example of the difference between declarative and imperative programming would be to compare the experience of building a web page statically with hand-crafted HTML (which is about as declarative as you can get):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;...
&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"things"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ol&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Thing 1&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Thing 2&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Thing 3&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ol&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... to building one dynamically by &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Document_object_model/How_to_create_a_DOM_tree" rel="noopener noreferrer"&gt;scripting the DOM&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ul&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ol&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;li&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;li&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;li&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Thing &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#things&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both yield the same result --- a three-item list --- but in fundamentally different ways. In HTML, the author says what they want, up front, and lets the browser handle the rest. In JavaScript, however, the author tells the browser &lt;em&gt;how&lt;/em&gt; to create that list, algorithmically, one element at a time before attaching it programmatically to the page at some point later.&lt;/p&gt;

&lt;p&gt;IaC tools vary similarly. Classically declarative tools like Arc, &lt;a href="https://aws.amazon.com/cloudformation/" rel="noopener noreferrer"&gt;CloudFormation&lt;/a&gt;, &lt;a href="https://www.terraform.io/" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt;, and others have you type out what you want, usually in some sort of structured configuration, and handle the work of provisioning and updating for you. Imperative tools don't do nearly as much; instead, they give &lt;em&gt;you&lt;/em&gt; the APIs to tell &lt;em&gt;them&lt;/em&gt; what to do and how to do it.&lt;/p&gt;

&lt;p&gt;As an example, imagine you wanted to create a couple of storage buckets on &lt;a href="https://aws.amazon.com/s3/" rel="noopener noreferrer"&gt;Amazon S3&lt;/a&gt;. To do that imperatively, you might reach for Amazon's &lt;a href="https://aws.amazon.com/sdk-for-javascript/" rel="noopener noreferrer"&gt;SDK for JavaScript&lt;/a&gt; and tap out a small imperative program like this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;S3Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CreateBucketCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ListBucketsCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-s3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;S3Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;us-west-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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 class="c1"&gt;// Name a couple of buckets.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;desiredBuckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bucket-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bucket-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`some-interestingly-named-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Imperatively create them, by calling the AWS S3 API directly.&lt;/span&gt;
    &lt;span class="nx"&gt;desiredBuckets&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CreateBucketCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Finally, list all buckets, including the two you just created.&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ListBucketsCommand&lt;/span&gt;&lt;span class="p"&gt;({}))).&lt;/span&gt;&lt;span class="nx"&gt;Buckets&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could run this program with Node.js (again, assuming your AWS creds were stashed &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html" rel="noopener noreferrer"&gt;in their proper locations&lt;/a&gt;), and in a few moments, produce the following result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;node index.js
&lt;span class="o"&gt;[&lt;/span&gt;
  &lt;span class="o"&gt;{&lt;/span&gt;
    Name: &lt;span class="s1"&gt;'some-interestingly-named-bucket-1'&lt;/span&gt;,
    CreationDate: 2021-03-08T18:00:04.000Z
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="o"&gt;{&lt;/span&gt;
    Name: &lt;span class="s1"&gt;'some-interestingly-named-bucket-2'&lt;/span&gt;,
    CreationDate: 2021-03-08T18:00:04.000Z
  &lt;span class="o"&gt;}&lt;/span&gt;,
&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nice, right? And easy enough --- assuming you're comfortable with JavaScript.&lt;/p&gt;

&lt;p&gt;However, unlike the Arc example I shared earlier, running the program a second time would fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;node index.js
UnhandledPromiseRejectionWarning: BucketAlreadyOwnedByYou
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... which is unfortunate, but makes sense, considering the buckets would already have been created. To keep repeated runs of the program from failing --- an important consideration, say, if the program were running as a part of an automated deployment process --- you'd have to write a bit more code to check for the existence of each bucket &lt;em&gt;before&lt;/em&gt; attempting to create it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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 class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;desiredBuckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bucket-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bucket-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`some-interestingly-named-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// First, fetch a list of all buckets.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allBuckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ListBucketsCommand&lt;/span&gt;&lt;span class="p"&gt;({}));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allBucketNames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;allBuckets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Buckets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&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 class="c1"&gt;// Create the new buckets...&lt;/span&gt;
    &lt;span class="nx"&gt;desiredBuckets&lt;/span&gt;

        &lt;span class="c1"&gt;// ...but only if they haven't been created already.&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;allBucketNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&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 class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CreateBucketCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that'd certainly work.&lt;/p&gt;

&lt;p&gt;But at the same time, all you really need is a couple of S3 buckets, here, and already you've begun to accumulate a good bit of code --- code that has to be debugged, tested, maintained, and all the rest. If you wanted to assemble something a little more complicated --- a couple of serverless endpoints, maybe, or the virtual infrastructure to run a typical web application --- you'd be looking at writing a lot &lt;em&gt;more&lt;/em&gt; code, and this pattern of checking &lt;em&gt;whether&lt;/em&gt; to do something before actually doing it (or doing something slightly different, maybe, under certain conditions) would continue to the point that it'd be hard for someone else (or even a future version of yourself) to look at the code and understand what was really going on --- certainly much harder than looking at a few lines of declarative YAML. Sometimes, of course, imperative code is just what you need. But for &lt;a href="https://en.wikipedia.org/wiki/Infrastructure_as_code#Added_value_and_advantages" rel="noopener noreferrer"&gt;lots of reasons&lt;/a&gt;, declarative tools are usually the right way to go --- which is why, as I said, the debate's pretty much over.&lt;/p&gt;

&lt;p&gt;Where does that leave Pulumi, though? If Pulumi programs really are written in imperative languages like JavaScript, doesn't that make Pulumi itself an imperative tool, too, by extension?&lt;/p&gt;

&lt;p&gt;In a word, no --- but understanding &lt;em&gt;why&lt;/em&gt; the answer is no takes a bit more explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breakfast as code
&lt;/h2&gt;

&lt;p&gt;I haven't always been a big breakfast person, but these days, I am, and for me, breakfast usually means an egg, some toast, and a bit of orange juice, with an occasional bunch of leafy-green things thrown in for good measure. Represented as JSON, my usual breakfast looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"breakfast"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"eggs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scrambled"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"toast"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"multi-grain"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"juice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orange"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a fairly common choice, as breakfasts go --- so common that I could probably walk into any café, hand someone this snippet of JSON, and wait patiently for the result to show up on the table in front of me. In a way, this is declarative breakfast-as-code: I say what I want --- egg, toast, juice --- and a bunch of skilled humans conspire to make that happen for me.&lt;/p&gt;

&lt;p&gt;And while I certainly &lt;em&gt;know&lt;/em&gt; there's an order in which these things tend to happen --- the eggs need scrambling, so the chef may prep them first; the toast goes quicker, so that'll probably happen later, etc. --- that order isn't important to &lt;em&gt;me&lt;/em&gt; as a customer. In the end, all I care about is that when breakfast is ready, it's hot, and on my plate. The JSON document just describes my &lt;em&gt;desired&lt;/em&gt; breakfast; it doesn't tell the chef or anyone else how to make it. That's what makes it declarative.&lt;/p&gt;

&lt;p&gt;Static text like JSON and YAML aren't the only ways to declare a desired breakfast, though. Here's a little JavaScript program that allocates a similar set of breakfast objects and relationships. Again, notice there isn't any &lt;em&gt;how&lt;/em&gt; going on, here --- we're still firmly in &lt;em&gt;what&lt;/em&gt; territory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Breakfast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Eggs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Juice&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;some-menu-or-something&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;breakfast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Breakfast&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;eggs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Eggs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scrambled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;multi-grain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;juice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Juice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;breakfast&lt;/code&gt; still consists of three things --- object instances of &lt;code&gt;Eggs&lt;/code&gt;, &lt;code&gt;Toast&lt;/code&gt;, and &lt;code&gt;Juice&lt;/code&gt; --- just as it did in the JSON representation. Assuming the constructors of these objects weren't doing anything fancy under the hood (just allocating local instance properties of their own, say), you'd expect that running this program with Node.js would produce, for a moment, a &lt;code&gt;breakfast&lt;/code&gt; variable referring to an instance of the &lt;code&gt;Breakfast&lt;/code&gt; class, and that the &lt;code&gt;breakfast&lt;/code&gt; instance would itself contain references to instances of each of its ingredients before the program finally exited. Without a doubt, this is imperative JavaScript &lt;em&gt;code&lt;/em&gt; --- but this particular expression is totally declarative; we've simply stated that &lt;code&gt;breakfast&lt;/code&gt; &lt;em&gt;depends&lt;/em&gt; on three ingredients, and left it up to the JavaScript engine to handle the dependent allocations and the order in which to perform them.&lt;/p&gt;

&lt;p&gt;As it happens, this a lot like &lt;a href="https://www.pulumi.com/docs/intro/concepts/how-pulumi-works/" rel="noopener noreferrer"&gt;how Pulumi works&lt;/a&gt;, too. A call to a Pulumi resource constructor (like &lt;a href="https://www.pulumi.com/docs/reference/pkg/aws/s3/bucket/" rel="noopener noreferrer"&gt;&lt;code&gt;new aws.s3.Bucket()&lt;/code&gt;, for example&lt;/a&gt;) is just an object declaration like any other, an expression of your desire to have an S3 bucket exist --- not to &lt;em&gt;create&lt;/em&gt; the S3 bucket &lt;em&gt;in that moment&lt;/em&gt;, but to &lt;em&gt;have&lt;/em&gt; it exist when the program completes. At runtime, the Pulumi SDK and engine conspire to gather up all of the object allocations in your program, figure out their relationships (which objects depend on which, what values they need from each other, and so on), assemble a JSON-serializable &lt;a href="https://en.wikipedia.org/wiki/Object_graph" rel="noopener noreferrer"&gt;object graph&lt;/a&gt; representing the full picture, and then use that graph to call on the cloud provider directly to produce the appropriate result. Just like with Arc and other &lt;em&gt;statically&lt;/em&gt; declarative tools, the code you write with Pulumi still says &lt;em&gt;what&lt;/em&gt;, not &lt;em&gt;how&lt;/em&gt;, and Pulumi takes care of delivering the outcome for you.&lt;/p&gt;

&lt;p&gt;Here's what it looks like to make a couple of S3 buckets with Pulumi and JavaScript, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/aws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bucket1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bucket1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bucket2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&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;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bucket2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you wanted, since you're working with JavaScript, you could even get a bit fancier by declaring the buckets with &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map" rel="noopener noreferrer"&gt;&lt;code&gt;Array#map&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&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;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`bucket&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the program once, and you get two buckets (along with a "&lt;a href="https://www.pulumi.com/docs/intro/concepts/stack/" rel="noopener noreferrer"&gt;stack&lt;/a&gt;," if you didn't already have one):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up

Updating &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;

     Type                 Name         Status
 +   pulumi:pulumi:Stack  buckets-dev  created
 +   ├─ aws:s3:Bucket     bucket1      created
 +   └─ aws:s3:Bucket     bucket2      created

Resources:
    + 3 created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it again, you get nothing, because the buckets you declared already exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up

Updating &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;

     Type                 Name
     pulumi:pulumi:Stack  buckets-dev

Resources:
    3 unchanged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could even reverse the sort order and still get the same result (since ultimately, it's up to Pulumi to determine what needs to be done, and how):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&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;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`bucket&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up

Updating &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;

     Type                 Name
     pulumi:pulumi:Stack  buckets-dev

Resources:
    3 unchanged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, that's declarative (and &lt;a href="https://en.wikipedia.org/wiki/Idempotence" rel="noopener noreferrer"&gt;idempotent!&lt;/a&gt;) infrastructure as code --- it just happens to have been written with an imperative programming language. You could modify this program to add a third bucket, remove a bucket, declare a JavaScript function to be invoked &lt;a href="https://www.pulumi.com/docs/guides/crosswalk/aws/lambda/" rel="noopener noreferrer"&gt;in response to a bucket event&lt;/a&gt;, whatever you want, it's always the same: Pulumi launches your chosen language runtime, listens for object allocations (by way of the &lt;code&gt;@pulumi/aws&lt;/code&gt; SDK, for example), registers those allocations with the &lt;a href="https://www.pulumi.com/docs/intro/concepts/how-pulumi-works/" rel="noopener noreferrer"&gt;engine&lt;/a&gt;, computes an in-memory graph of resources and relationships, and then calls on your cloud provider directly to issue the appropriate set of changes, in the right order.&lt;/p&gt;

&lt;p&gt;Great --- so now you know how Pulumi works.&lt;/p&gt;

&lt;p&gt;But it's still worth asking: is all of this really &lt;em&gt;necessary?&lt;/em&gt; What kinds of problems does Pulumi actually solve? What makes this "imperatively declarative" approach to infrastructure worth the additional layers of indirection --- the language, runtime, dependencies, and the rest? Wouldn't it be easier just to write a few lines of YAML and be done than to have to contend with all of this extra &lt;em&gt;stuff?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Sure --- for simple things, maybe. But software has a funny way of starting out simple and suddenly becoming annoyingly complex ---  often a lot sooner than you think.&lt;/p&gt;

&lt;h2&gt;
  
  
  When breakfast gets complicated
&lt;/h2&gt;

&lt;p&gt;For me, thanks to my basic breakfast needs, getting what I want is usually no big deal. That's because most cafés are going to have eggs, bread, and orange juice on hand and ready to make --- and also because I'm not all that fussy about the details.&lt;/p&gt;

&lt;p&gt;But for my family, it's more complicated. I have three kids, for example, all of whom have mild food sensitivities, and a wife who rarely eats out because of how hard it is to find something she likes. None of them could walk into a diner with an order like mine, because they'd need to be able to ask certain questions first: &lt;em&gt;Are the eggs made with milk? Are the waffles gluten-free?&lt;/em&gt; Each of these questions needs to be answered, for real and important reasons, before our collective order can be submitted and fulfilled.&lt;/p&gt;

&lt;p&gt;It'd be impossible, in other words, to walk into a restaurant with a handwritten order for a family like ours expecting to have it accepted verbatim without some kind of interaction first. &lt;em&gt;Oh, the waffles aren't gluten-free? Okay --- we'll take an omelet instead.&lt;/em&gt; It's always something, and I imagine it's probably like that for most of us: we know what we want, and we're usually able to get it, but not without a little negotiation during the process. At a high level, we know want "breakfast", which is easy. But in practice, we almost always end up having to apply some sort of algorithm, however simple, during that process.&lt;/p&gt;

&lt;p&gt;In fact, that's kind of how &lt;em&gt;everything&lt;/em&gt; works, software included --- and infrastructure (especially the cloud-based kind) is nothing not fundamentally software. If all you need is a couple of storage buckets or Lambdas or VMs, sure, you can kick out that stuff with a few lines of YAML and get on with your day --- and that's awesome, to be sure. But more often, what you'll find is that you'll eventually need &lt;em&gt;something more&lt;/em&gt;, some tiny bit of customization or other that the simple tool can't &lt;em&gt;quite&lt;/em&gt; give you out of the box --- and that's when the trouble begins.&lt;/p&gt;

&lt;p&gt;When the problem is straightforward and well bounded, in other words, simple tools are great, and often more than enough to get the job done. But when the problem is even a little bit complicated, or when the problem &lt;em&gt;space&lt;/em&gt; expands beyond what those simple tools were originally designed for, the tools themselves will tend to bend and crack in the places that weren't really made with complexity in mind.&lt;/p&gt;

&lt;p&gt;Take our two buckets, for example. If you knew how many buckets you wanted to create and how you wanted to name them, you could do that fairly easily with &lt;a href="https://www.terraform.io/docs/language/syntax/configuration.html" rel="noopener noreferrer"&gt;HCL&lt;/a&gt;, the config language of Terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-west-2"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"buckets"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"bucket"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"some-interestingly-named-bucket-${var.buckets[count.index]}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're not familiar with HCL, you might need to squint to figure out what's going on here, but it's a lot like our first bucket-provisioning example from earlier: we just loop through a list of strings ("1", "2", and "3"), creating a bucket for each one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;terraform apply

aws_s3_bucket.bucket[1]: Creating...
aws_s3_bucket.bucket[2]: Creating...
aws_s3_bucket.bucket[0]: Creating...
aws_s3_bucket.bucket[0]: Creation &lt;span class="nb"&gt;complete &lt;/span&gt;after 3s &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;some-interestingly-named-bucket-1]
aws_s3_bucket.bucket[1]: Creation &lt;span class="nb"&gt;complete &lt;/span&gt;after 3s &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;some-interestingly-named-bucket-2]
aws_s3_bucket.bucket[2]: Creation &lt;span class="nb"&gt;complete &lt;/span&gt;after 3s &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;some-interestingly-named-bucket-3]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, this totally works --- assuming the names you've chosen &lt;a href="https://www.pulumi.com/docs/intro/concepts/resources/#autonaming" rel="noopener noreferrer"&gt;are globally unique&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now imagine you had to name those buckets in a slightly more complicated way --- using stringified date, perhaps. Naming a bucket dynamically with a format string like &lt;code&gt;YYYY-MM-DD&lt;/code&gt; is maybe &lt;em&gt;possible&lt;/em&gt; with Terraform (or if not, maybe using a bit of &lt;a href="https://en.wikipedia.org/wiki/Shell_script" rel="noopener noreferrer"&gt;shell scripting&lt;/a&gt; with and an HCL &lt;code&gt;variable&lt;/code&gt;), but you'd definitely be running into the limits of what HCL is able to do on its own. That's not a knock against HCL, either: every special-purpose language runs the risk of hitting these kinds of limitations eventually.&lt;/p&gt;

&lt;p&gt;With general-purpose languages like JavaScript, though, this kind of thing is trivially easy, either with the language alone or with the help of a third-party package to make things even easier --- one like &lt;a href="https://github.com/iamkun/dayjs" rel="noopener noreferrer"&gt;Day.js&lt;/a&gt;, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/aws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;dayjs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dayjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Keep a bucket for each of the last 7 days.&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&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;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;subtract&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;day&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YYYY-MM-DD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pulumi up

Updating &lt;span class="o"&gt;(&lt;/span&gt;dev&lt;span class="o"&gt;)&lt;/span&gt;

     Type                 Name         Status
 +   pulumi:pulumi:Stack  buckets-dev  created
 +   ├─ aws:s3:Bucket     2021-03-24   created
 +   ├─ aws:s3:Bucket     2021-03-29   created
 +   ├─ aws:s3:Bucket     2021-03-28   created
 +   ├─ aws:s3:Bucket     2021-03-27   created
 +   ├─ aws:s3:Bucket     2021-03-25   created
 +   ├─ aws:s3:Bucket     2021-03-23   created
 +   └─ aws:s3:Bucket     2021-03-26   created

Resources:
    + 8 created

Duration: 9s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you carve away the language, Pulumi and Terraform are doing a lot of the same things: both work to assemble graphs of resources and dependencies, both use those graphs to communicate with cloud providers directly, and both manage state in conceptually similar ways. It's at the language layer --- and up --- that they really start to diverge.&lt;/p&gt;

&lt;p&gt;Again, how much that matters is for you to decide. But as a developer, I'll take a full programming language (especially one I know well) any day of the week, because it means I can do anything the language &lt;em&gt;and its ecosystem&lt;/em&gt; can do, and that I probably won't end up in tears in six months when I'm faced with a problem that my tools can't handle. Just yesterday, for example, I found myself wrestling with &lt;a href="https://www.gnu.org/software/bash/" rel="noopener noreferrer"&gt;Bash&lt;/a&gt; trying to move a few files between Git repositories. After a frustrating couple of hours of hacking and Googling, I realized I could just use Node.js instead --- and when I did, I was done in a matter of minutes. An expert shell programmer might've made light work of what I was trying to do --- but I'm not an expert shell programmer, and Bash isn't JavaScript. All it took was a couple of Node.js built-ins and libraries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ yarn add glob micromatch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... and eight lines of JavaScript later, I was done.&lt;/p&gt;

&lt;p&gt;For me, language --- and all that comes with it --- is ultimately what it's all about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Like React for infrastructure
&lt;/h2&gt;

&lt;p&gt;All of this reminds me of the progression we've seen over the last two decades in web development.&lt;/p&gt;

&lt;p&gt;Think of &lt;a href="https://reactjs.org/" rel="noopener noreferrer"&gt;React&lt;/a&gt;. Why do we have it? Because HTML alone isn't enough, and imperative DOM scripting leads to reams of unmaintainable code. We got React because we, as developers, wanted to &lt;em&gt;think&lt;/em&gt; about, and compose, our front-end applications in declarative ways --- but we &lt;em&gt;needed&lt;/em&gt; to retain the flexibility of the JavaScript language. So we got React --- and with it, an imperatively declarative programming model for the web:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Imperative code...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Akron&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Nashua&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Rochester&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Scranton&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Syracuse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Utica&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DunderMifflinBranchOffices&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... declaratively rendered...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ul&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;offices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;office&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;office&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;                &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;office&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Scranton&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="nx"&gt;The&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="nx"&gt;one&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&amp;gt; &lt;/span&gt;&lt;span class="err"&gt;}
&lt;/span&gt;            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/li&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;)
&lt;/span&gt;        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/ul&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;...
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;aside&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;nav&amp;gt;&lt;/span&gt;
            &lt;span class="c"&gt;&amp;lt;!-- ... and composed. --&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;DunderMifflinBranchOffices&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's the same thing with infrastructure: we want a declarative mental model, but we need the control and composabilty of general-purpose languages. Hence tools like Pulumi.&lt;/p&gt;

&lt;p&gt;It'll be interesting to see where things go from here; &lt;a href="https://dev.to/about"&gt;I'm&lt;/a&gt; &lt;a href="https://www.pulumi.com/about/" rel="noopener noreferrer"&gt;certainly&lt;/a&gt; &lt;a href="https://thepulumibook.com/" rel="noopener noreferrer"&gt;biased&lt;/a&gt;, but also a fascinated observer. The trajectory is what interests me most, though --- that, and being able to manage my own infrastructure in ways that feel comfortable to me as a developer.&lt;/p&gt;

</description>
      <category>pulumi</category>
      <category>infrastructure</category>
      <category>javascript</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
