Deploying production-ready containers with Pulumi

Posted on

Containers are a great way to deploy applications to the cloud, especially with new execution models like AWS Fargate. Pulumi makes it easy to deploy production Docker containers, handling details such as creating a container registry instance in ECR, creating task definitions in ECS, and configuring a load balancer. With Pulumi, deploying a container to production is almost as easy as running it locally!

In this blog post, we’ll deploy a simple Docker container running NGINX.

Setup

If this is your first time using Pulumi, go to https://app.pulumi.com and sign in with GitHub.

Then, run the following command to install the Pulumi CLI:

$ curl -fsSL https://get.pulumi.com/ | sh

If you’re on Windows, run this:

@"%SystemRoot%System32WindowsPowerShell1.0powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://get.pulumi.com/install.ps1'))"
SET "PATH=%PATH%;%USERPROFILE%.pulumiin"

You’ll deploy this app to your own AWS account, so follow the steps to configure your AWS account.

Make sure you have Node.js installed, with a version of 6.10.x or later.

Finally, make sure Docker is installed and running.

Create the app

We’ll create a Pulumi project, define the infrastructure in JavaScript, and create a Dockerfile.

To create a new Pulumi project, run the following commands:

mkdir hello-containers && cd hello-containers
pulumi new aws-javascript

This creates a new project in the directory hello-containers. Next, replace the contents of index.js with the following:

const cloud = require("@pulumi/cloud-aws");
let service = new cloud.Service("pulumi-nginx", {
    containers: {
        nginx: {
            build: "./app",
            memory: 128,
            ports: [{ port: 80 }],
        },
    },
    replicas: 2,
});

/* export just the hostname property of the container frontend */
exports.url = service.defaultEndpoint.apply(e => `http://${e.hostname}`);

These 15 lines of code are everything you need to deploy a custom container! We’re using cloud.Service, which is a high-level, convenient interface for building containers and provisioning an AWS container service. Using the build property, we point to a folder containing a Dockerfile, which is app in this case.

Now, let’s create a Dockerfile and a static page. Create a subfolder app with the following files.

Add the following as Dockerfile:

FROM nginx

COPY index.html /usr/share/nginx/html

Add the following as index.html:

<html>
    <head>
    <title>Hello World</title>
    <meta charset="UTF-8">
    </head>
    <body>
    <p>Hello containers!</p>
    <p>Made with ❤️ with <a href="https://pulumi.com">Pulumi</a></p>
    </body>
</html>

You should have the following directory structure:

file layout

  1. Install the @pulumi/cloud-aws NPM package:

    npm install –save @pulumi/cloud-aws @pulumi/cloud

  2. Configure Pulumi to use AWS Fargate. Note that, currently, Fargate is available only in us-east-1, us-east-2, us-west-2, and eu-west-1.

    pulumi config set cloud-aws:useFargate true

Deploy the app

To deploy both the infrastructure and app code, we’ll run pulumi update. This command first shows a preview of all the resources that will be created and prompts for confirmation. During the preview phase, Pulumi invokes docker build.

Pulumi preview

Choose the yes option to deploy to AWS. This will take about 5 minutes. Pulumi automatically builds and provisions an AWS container repository in ECR, builds the Docker container, and places the image in the repository. This all happens automatically and does not require manual configuration on your part.

At the end of the update, you’ll see a link to the Pulumi Service that shows the details of the deployment.

update complete

Go to this link and click the Resources tab. You’ll see all the resources you’ve created. Notice that Pulumi has created an ECR repository, a load balancer, an ECS service and task definition, and IAM roles.

Now, go back to the stack details page, which shows the stack configuration, as well as the stack output property. The following line creates the stack output url:

exports.url = service.defaultEndpoint.apply(e => `http://${e.hostname}`);

If you navigate to the link for url you’ll see the following page:

&ldquo;Hello, World!&rdquo;

View logs

You can view container frontend and compute logs via the pulumi logs command. You can see here that most of the traffic is hitting routes that don’t exist, aside from the last log line where I navigated to the site using Chrome. Since this is an HTTP server that’s open to the internet, it’s not uncommon for attackers to try to find a security vulnerability on a site. Fortunately, our container is serving only static content. The logs command makes it easy to get details on your site traffic.

$ pulumi logs
Collecting logs for stack hello-containers-dev since 2018-06-15T15:01:18.000-07:00.

 2018-06-15T15:27:47.468-07:00[                  pulumi-nginx] 2018/06/15 22:27:47 [error] 5#5: *4 open() "/usr/share/nginx/html/proxychecker.axd" failed (2: No such file or directory), client: 62.210.157.152, server: localhost, request: "GET http://proxy.dazhou.net/proxychecker.axd?e=457758511%40qq.com&p=http%3A%2F%2F34.201.27.186%3A80&s=AS HTTP/1.1", host: "proxy.dazhou.net"
 2018-06-15T15:27:47.468-07:00[                  pulumi-nginx] 62.210.157.152 - - [15/Jun/2018:22:27:47 +0000] "GET http://proxy.dazhou.net/proxychecker.axd?e=457758511%40qq.com&p=http%3A%2F%2F34.201.27.186%3A80&s=AS HTTP/1.1" 404 170 "-" "Go-http-client/1.1" "-"
 2018-06-15T15:32:02.128-07:00[                  pulumi-nginx] 2018/06/15 22:32:02 [error] 5#5: *382 "/usr/share/nginx/html/phpmyadmin/index.html" is not found (2: No such file or directory), client: 178.239.177.212, server: localhost, request: "HEAD http://34.201.27.186:80/phpmyadmin/ HTTP/1.1", host: "34.201.27.186"
 2018-06-15T16:07:05.874-07:00[                  pulumi-nginx] 172.31.44.144 - - [15/Jun/2018:23:07:05 +0000] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36" "-"

Clean up

To clean up the resources we’ve provisioned, run pulumi destroy.

Next steps

The sample code for this post is available in the Pulumi examples repo on GitHub. For an example application that connects two containers, see the Voting App TypeScript sample.