Creating and Reusing Cloud Components using Package Managers

Posted on

Hello! A few weeks back I wrote a post on serving static websites on AWS with Pulumi detailing how to host a static website on AWS. Pulumi allowed me to wire four different AWS products together in only 200 lines of code. It would be a shame, however if I needed to copy and paste that code every time I wanted to to stand up a new website. Instead, we can package up, share, and reuse our code just like any other Node.js library. It just so happens that this one can be used to create cloud infrastructure.

Creating a Package

To create a reusable package for the static website example, I simply put the code into a new GitHub repo and publish it to NPM. We’ll walk through exactly how to do that below, but afterwards anyone who wants to stand up a static website on AWS can just reuse that package.

The static-website-aws package exports a StaticWebsite class, which is a logical, reusable cloud “component” resource. This class offers a simple constructor that internally does all of the hard work of creating the assortment of AWS cloud resources in the prior blog post – an S3 Bucket and objects per piece of static content, CloudFront Distribution, and Route53 DNS A-Record – and exposes them afterwards as readonly properties:

/**
 * Static website using Amazon S3, CloudFront, and Route53.
 */
export declare class StaticWebsite extends pulumi.ComponentResource  {
    readonly contentBucket: aws.s3.Bucket;
    readonly logsBucket: aws.s3.Bucket;
    readonly cdn: aws.cloudfront.Distribution;
    readonly aRecord?: aws.route53.Record;

    /**
    * Creates a new static website hosted on AWS.
    * @param name  The _unique_ name of the resource.
    * @param contentArgs  The arguments to configure the content being served.
    * @param domainArgs  The arguments to configure the domain and DNS settings.
    * @param opts  A bag of options that control this resource's behavior.
    */

    constructor(name: string , contentArgs: ContentArgs,
                domainArgs?: DomainArgs, opts?: pulumi.ResourceOptions);
}

This class is placed into an NPM package as usual, including a package.json file that gives it a name and a version. After doing this, we can publish it to NPM simply by running

$ npm publish

All the complexities of creating, configuring, and wiring up the necessary AWS resources – admittedly only 200 lines of code, but still intricate – has now been hidden beneath an ultra-simple component resource named StaticWebsite.

That’s how you create a new package. The great thing about packages, however, is that you define them once and then use them a lot. So now let’s turn to see see how standing up a new CDN-backed website can now become just a few lines of very simple, understandable code.

Reusing Infrastructure as Code

To use this new package, head on over to a Pulumi program, or create a new one (for instance, with pulumi new aws-typescript). Then just add a reference like any other dependency:

$ npm install static-website-aws

At that point, we can import our StaticWebsite class from the static-website-aws package like usual in Node.js programs, either using require

let swa = require("static-website-aws");

or using the new ES6 module import syntax, supported by TypeScript and modern JavaScript

import { StaticWebsite } from "static-website-aws";

Afterwards, we’ll just new up a StaticWebsite object, which internally creates everything for us. Of course, to create a useful website, we’ll need to supply some interesting parameters . In this case, let’s just pass a path to the content on disk (which will automatically get uploaded into S3 objects during a pulumi up) in addition to a path to a custom 404 HTML page:

import  { StaticWebsite } from "static-website-aws";

const website  = new StaticWebsite ("browserhack", {
    pathToContent:"./browserhack",
    custom404Path:"/404.html",
});

export let cloudfrontDomain  = website .cdn .domainName ;

As soon as we have our program, we can stand it up with a single pulumi up  command:

$ pulumi up

This will show us the entire resource graph so that we can see what it’s creating internally and once it’s done the resulting domain name will be printed out:

pulumi cli output

Let’s then curl it to see whether it worked:

$ curl $(pulumi stack output cloudfrontDomain)`[<!doctype html>
]{style="color: red; font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; font-size: 13px;"}[<!--
vim: set sw=2 ts=2 et : -->
]{style="color: red; font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; font-size: 13px;"}[<html>
]{style="color: red; font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; font-size: 13px;"}[
 <head>
]{style="color: red; font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; font-size: 13px;"}[...
etc, etc, etc
...]{style="color: red; font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; font-size: 13px;"}

Huzzah! (Note that new CloudFront Domains can take a while to spin up; if you get an error “Could not resolve host: xxx.cloudfront.net”, just wait a little bit and try again.)

The full source code for this example is available at chrsmith/browserhack-demo. This program deploys an instance of BrowserHack (a web-based port of the seminal console game NetHack). The repo also includes an example of assigning a custom domain name and ACM-managed SSL certificate, useful capabilities supported by StaticWebsite but omitted here for brevity .

Resource Components

We glossed over what components are earlier on. This is an advanced concept, so feel free to skip this section; but if you want to understand how components work under the hood, read on.

The StaticWebsite type extends pulumi.ComponentResource. A ComponentResource is a Pulumi-managed resource that aggregates other resources into a higher-level abstraction.

Pulumi will manage creating and updating the underlying resources like normal, and you will see updates to them in the resource tree. The difference is that by using the ComponentResource abstraction we can present those resources as logically grouped into a new, static website type.

This parent/child relationship between resources is declared using the optional pulumi.ResourceOptions type passed to Pulumi resources.

Here’s a code snippet from the code, where we create the S3 bucket and specify its logical parent resource. Every child resource created is passed defaultResourceOptions, which sets the parent property to this (the instance of StaticWebsite ).

// Default resource options for this component's child resources.
const defaultResourceOptions: pulumi.ResourceOptions = { parent:this };

...

// Create the logs bucket to store CloudFront request logs.

this.logsBucket = new aws.s3.Bucket(`${name }-logs`,
    { acl:"private" },
    defaultResourceOptions,
);

Components are a powerful construct in Pulumi, and let you build larger first class resources out of smaller ones. There are other benefits, including seeing resource attribution in the CLI tree view, and the richer visualization in the Pulumi Service:

pulumi app graph

If you want to learn more about components, see the documentation.

Package Everything!

In this blog, you’ve seen how Pulumi’s code-centric approach to infrastructure can make you more productive programming the cloud. “Code” has a lot of advantages over static configuration languages, for example rich IDE support. Another big advantage of using code, however, is that it allows you to create useful abstractions that can be reused in other programs

With Pulumi being able to take advantage of existing package managers, it’s super easy to create high-quality reusable components. We demonstrated NPM here for Node.js programs, but Pulumi also supports PyPI for Python, and traditional library techniques for Go.

But don’t just take our words for it. Recently Mikhail Shilkov blogged about creating a Pulumi component to keep AWS Lambda functions warm. And James Nugent open sourced a component that creates a properly subnetted AWS VPC. This is a great way to encode and share best practices broadly. Over time, we look forward to seeing the many other battle-tested components the community creates for common cloud applications and infrastructure needs.

If you’ve created a Pulumi component that is useful or want some design advice, come and join us in the Pulumi Community Slack – we’d love to hear from you!