DEV Community

Cover image for How to Deploy a Next.Js App to AWS ECS with HashiCorp Waypoint
Kyle Galbraith
Kyle Galbraith

Posted on • Updated on • Originally published at blog.kylegalbraith.com

How to Deploy a Next.Js App to AWS ECS with HashiCorp Waypoint

Last week HashiCorp launched its latest open source project, Waypoint. Waypoint is a tool to streamline the build, deploy, and release workflow for all kinds of applications. Where Terraform focuses on provisioning infrastructure, Waypoint focuses on application deployment.

It's an interesting new deployment tool. In this post, we are going to take waypoint for a spin and see what it is like to use it.

Prerequisites

Before we can dive into waypoint, we need to have a few things set up. First, we are going to use Terraform to create our container image repository and ECS cluster in AWS. So make sure you have terraform configured, see these docs for details.

Next, we need to have our AWS CLI configured. These docs show you how to get your CLI configured.

Finally, we need to install waypoint. This can be done a number of ways as these docs mention. If you're on a Mac, you can run these two commands in brew to get it installed.

$ brew tap hashicorp/tap
$ brew install hashicorp/tap/waypoint
Enter fullscreen mode Exit fullscreen mode

Setting up our Next.js demo application

Before we can focus on deployment, we need an actual application to deploy. Let's create a sample Next.js application that we can deploy via Waypoint.

We are going to make use of create-next-app. You can install it globally via npm install -g create-next-app. We will create our demo application based on the with-typescript example.

$ yarn create next-app --example with-typescript waypoint-demo
Enter fullscreen mode Exit fullscreen mode

If we go into the waypoint-demo directory and run yarn dev we should have a functioning demo Next.js application living at localhost:3000.

Next.js demo application

Setting up our Terraform & Waypoint structure

With our prerequisites out of the way and a demo application to deploy, let's set up our configuration for Terraform and Waypoint. From the waypoint-demo directory, run the init command via waypoint.

$ cd waypoint-demo
$ waypoint init
Enter fullscreen mode Exit fullscreen mode

Awesome, we should now see a waypoint.hcl file inside of our waypoint-demo directory.

Waypoint also makes use of a server that you run locally in order for the CLI and GUI to work. This is a bit clunky at the moment but we need to do the following two things to run the Waypoint server locally.

$ docker pull hashicorp/waypoint:latest
$ waypoint install --platform=docker -accept-tos
โœ“ Server container started
Waypoint server successfully installed and configured!

The CLI has been configured to connect to the server automatically. This
connection information is saved in the CLI context named "install-1602801878".
Use the "waypoint context" CLI to manage CLI contexts.

The server has been configured to advertise the following address for
entrypoint communications. This must be a reachable address for all your
deployments. If this is incorrect, manually set it using the CLI command
"waypoint server config-set".

Advertise Address: waypoint-server:9701
HTTP UI Address: localhost:9702
Enter fullscreen mode Exit fullscreen mode

The latter step launches the Waypoint server locally using Docker.

Next up, we need a directory to hold our Terraform configuration and state. Create a new folder in waypoint-demo called infrastructure and add a file called versions.tf.

$ cd waypoint-demo
$ mkdir infrastructure
$ cd infrastructure
$ touch versions.tf
Enter fullscreen mode Exit fullscreen mode

Great, we now have a place to land both our Waypoint and Terraform configuration.

Creating our infrastructure with Terraform

We are going to use Terraform to define the infrastructure for our application. In the context of this blog post, that means our AWS resources that our Next.js application is going to run on. For this post, we are going to use AWS Elastic Container Service (ECS) to run our application.

To do that we first need to provision a new ECS cluster in our AWS account. So we are going to add the following to our versions.tf file inside of our infrastructure folder.

terraform {
  required_version = ">= 0.13"
}

provider "aws" {
  region = "us-west-2"
}

resource "aws_ecs_cluster" "nextjs-cluster" {
  name = "waypoint-nextjs-cluster"
}

Enter fullscreen mode Exit fullscreen mode

This will use the AWS provider in Terraform to provision a new ECS cluster in our account with the name waypoint-nextjs-cluster. Let's go ahead and run terraform apply to provision our cluster.

$ cd waypoint-demo/infrastructure
$ terraform init
$ terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_ecs_cluster.nextjs-cluster will be created
  + resource "aws_ecs_cluster" "nextjs-cluster" {
      + arn  = (known after apply)
      + id   = (known after apply)
      + name = "waypoint-nextjs-cluster"

      + setting {
          + name  = (known after apply)
          + value = (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_ecs_cluster.nextjs-cluster: Creating...
aws_ecs_cluster.nextjs-cluster: Still creating... [10s elapsed]
aws_ecs_cluster.nextjs-cluster: Creation complete after 10s [id=arn:aws:ecs:us-west-2:<aws-id>:cluster/waypoint-nextjs-cluster]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ช We now have our AWS ECS cluster that we are going to run our application in. Next up we need a container image registry for Waypoint to publish to. We can use AWS Elastic Container Registry (ECR) for that.

We can add an ECR resource to our Terraform configuration in versions.tf. Add the following resource to the bottom of that file.

resource "aws_ecr_repository" "nextjs-image-repo" {
  name = "nextjs-image"
}
Enter fullscreen mode Exit fullscreen mode

We can run terraform apply one more time to create our image repository.

$ terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_ecr_repository.nextjs-image-repo will be created
  + resource "aws_ecr_repository" "nextjs-image-repo" {
      + arn                  = (known after apply)
      + id                   = (known after apply)
      + image_tag_mutability = "MUTABLE"
      + name                 = "nextjs-image"
      + registry_id          = (known after apply)
      + repository_url       = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_ecr_repository.nextjs-image-repo: Creating...
aws_ecr_repository.nextjs-image-repo: Creation complete after 1s [id=nextjs-image]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Enter fullscreen mode Exit fullscreen mode

Using Waypoint to deploy our application

Now that we have the AWS resources we need to run our Next.js app, let's deploy it using Waypoint!

To leverage waypoint we need to add our configuration to waypoint.hcl. We are going to focus on building our Next.js app as a container image and deploying it to our running ECS cluster.

Let's go ahead and add the following to our waypoint.hcl file.

project = "example-next-ecs"

app "next-ecs" {
  build {
    use "pack" {}
    # Use ECR docker registry provisioned via infrastructure/versions.tf
    registry {
      use "aws-ecr" {
        region     = "us-west-2"
        repository = "nextjs-image"
        tag        = "latest"
      }
    }

  }

  # Deploy to ECS
  deploy {
    use "aws-ecs" {
      cluster = "waypoint-nextjs-cluster"
      region  = "us-west-2"
      memory  = "512"
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

What in the world are we doing here?

First, we define an app for Waypoint, next-ecs. Inside of our app, we then define a build step and a deploy step. With our build step we are making use of Cloud Native Buildpacks plugin via the use "pack" {} block. This is in favor of using Docker, but Docker is supported inside of Waypoint as well. We then define our registry block. This is used by the build step in Waypoint to push the built container image to a remote repository. This remote repository is the ECR repository we provisioned via Terraform.

Inside of the deploy block we make use of the aws-ecs plugin for Waypoint. Here we specify the ECS cluster we want to deploy our container to. Again, this is the cluster we provisioned earlier via Terraform. We also specify the memory we want our container to have, 512 MB.

Now that we understand what is going on, let's deploy it. We first need to initialize Waypoint and then we can run the up command.

$ cd waypoint-demo
$ waypoint init
$ waypoint up
ยป Building...
Creating new buildpack-based image using builder: heroku/buildpacks:18
โœ“ Creating pack client
โœ“ Building image
 โ”‚ [exporter] Reusing layer 'config'
 โ”‚ [exporter] Adding label 'io.buildpacks.lifecycle.metadata'
 โ”‚ [exporter] Adding label 'io.buildpacks.build.metadata'
 โ”‚ [exporter] Adding label 'io.buildpacks.project.metadata'
 โ”‚ [exporter] *** Images (fa42ccc82d85):
 โ”‚ [exporter]       index.docker.io/library/next-ecs:latest
 โ”‚ [exporter] Reusing cache layer 'heroku/nodejs-engine:nodejs'
 โ”‚ [exporter] Reusing cache layer 'heroku/nodejs-engine:toolbox'
 โ”‚ [exporter] Reusing cache layer 'heroku/nodejs-engine:yarn'
 โ”‚ [exporter] Reusing cache layer 'heroku/nodejs-yarn:node_modules'
โœ“ Injecting entrypoint binary to image

Generated new Docker image: next-ecs:latest
Creating new buildpack-based image using builder: heroku/buildpacks:18โœ“ Creating pack client
โœ“ Building image
 โ”‚ [exporter] Reusing layer 'config'
 โ”‚ [exporter] Adding label 'io.buildpacks.lifecycle.metadata'
 โ”‚ [exporter] Adding label 'io.buildpacks.build.metadata'
 โ”‚ [exporter] Adding label 'io.buildpacks.project.metadata'
 โ”‚ [exporter] *** Images (fa42ccc82d85):
 โ”‚ [exporter]       index.docker.io/library/next-ecs:latest
 โ”‚ [exporter] Reusing cache layer 'heroku/nodejs-engine:nodejs'
 โ”‚ [exporter] Reusing cache layer 'heroku/nodejs-engine:toolbox'
 โ”‚ [exporter] Reusing cache layer 'heroku/nodejs-engine:yarn'
 โ”‚ [exporter] Reusing cache layer 'heroku/nodejs-yarn:node_modules'
โœ“ Injecting entrypoint binary to image

Generated new Docker image: next-ecs:latest
Tagging Docker image: next-ecs:latest => <aws-id>.dkr.ecr.us-west-2.amazonaws.com/nextjs-image:latest
Docker image pushed: <aws-id>.dkr.ecr.us-west-2.amazonaws.com/nextjs-image:latest

ยป Deploying...
โœ“ Found existing ECS cluster: waypoint-nextjs-cluster
โœ“ Found existing IAM role to use: ecr-next-ecs
โœ“ Created ALB target group
โœ“ Modified ALB Listener to introduce target group
โœ“ Configured security group: next-ecs-inbound-internal
โœ“ Created ECS Service (next-ecs-N345T9YF471RDNX395EXZE4, cluster-name: waypoint-nextjs-cluster)

ยป Releasing...

The deploy was successful! A Waypoint deployment URL is shown below. This
can be used internally to check your deployment and is not meant for external
traffic. You can manage this hostname using "waypoint hostname."

   Release URL: http://waypoint-ecs-next-ecs-708892391.us-west-2.elb.amazonaws.com
Deployment URL: https://violently-comic-wolf--v7.waypoint.run
Enter fullscreen mode Exit fullscreen mode

After running up we should be able to hit the Release URL provided by Waypoint to see our running application.

Running app after Waypoint deployment

Cleanup

Now that we have things running and we know how to get a Next.js running in AWS via Waypoint, let's clean up all our resources. This is important to do so that we avoid spending unnecessary $$$ for a simple demo such as this.

Cleanup

To clean everything up we need to run two commands, one for Terraform and one for Waypoint.

First, we run waypoint destroy to clean up all our app related resources. Waypoint will remove the service that got created inside of the ECS cluster and nothing else.

$ cd waypoint-demo
$ waypoint destroy
ยป Destroying deployments...
Destroy successful!
Enter fullscreen mode Exit fullscreen mode

Then we need to run terraform destroy from our infrastructure folder. This removes the ECS cluster and ECR repository that we created earlier.

$ cd waypoint-demo/infrastructure
$ terraform destroy
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_ecr_repository.nextjs-image-repo will be destroyed
  - resource "aws_ecr_repository" "nextjs-image-repo" {
      - arn                  = "arn:aws:ecr:us-west-2:249704159252:repository/nextjs-image" -> null
      - id                   = "nextjs-image" -> null
      - image_tag_mutability = "MUTABLE" -> null
      - name                 = "nextjs-image" -> null
      - registry_id          = "249704159252" -> null
      - repository_url       = "249704159252.dkr.ecr.us-west-2.amazonaws.com/nextjs-image" -> null
      - tags                 = {} -> null

      - encryption_configuration {
          - encryption_type = "AES256" -> null
        }

      - image_scanning_configuration {
          - scan_on_push = false -> null
        }
    }

  # aws_ecs_cluster.nextjs-cluster will be destroyed
  - resource "aws_ecs_cluster" "nextjs-cluster" {
      - arn                = "arn:aws:ecs:us-west-2:249704159252:cluster/waypoint-nextjs-cluster" -> null
      - capacity_providers = [] -> null
      - id                 = "arn:aws:ecs:us-west-2:249704159252:cluster/waypoint-nextjs-cluster" -> null
      - name               = "waypoint-nextjs-cluster" -> null
      - tags               = {} -> null

      - setting {
          - name  = "containerInsights" -> null
          - value = "disabled" -> null
        }
    }

Plan: 0 to add, 0 to change, 2 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

Destroy complete! Resources: 2 destroyed.
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this post, we took Waypoint through a quick shakedown. We provisioned our infrastructure via Terraform. Then via a quick waypoint.hcl file we were able to deploy our application. Next.js was one example of an application that we can deploy but the options are rather limitless either.

We could define a Java application and deploy that as a container. We could deploy our applications to Kubernetes instead of an ECS cluster. We could build container images via Docker instead Cloud Native Buildpacks.

The moral of the story is that Waypoint is leaving the door open for flexibility. You can use any application or underlying infrastructure with it, in theory.

It's early days for Waypoint (it did just get released this week with a 0.1.1 release). That means there are some rough edges.

For instance, while writing this blog post I noticed that Waypoint did not work with existing ECS clusters even though the documentation said it did.

In theory, Waypoint will support any type of application or underlying infrastructure. In its current form, that's not quite true. There are only a small handful of plugins for building and deploying applications. So things are a bit limited.

But, Waypoint is built on the same plugin concept as Terraform. This means that 3rd party providers will be able to create their own plugins for Waypoint. So if providers add their plugins, you will be able to deploy any application to any underlying infrastructure.

As a fan of HashiCorp offerings, I am excited to see what Waypoint holds for the future. There are many ways to deploy your applications today to wherever your compute lives. Waypoint is interesting. It is providing an opinionated approach that looks to provide structure while also leaving room for flexibility.

Want to check out my other projects?

I am a huge fan of the DEV community. If you have any questions or want to chat about different ideas relating to refactoring, reach out on Twitter or drop a comment below.

Outside of blogging, I created a Learn AWS By Using It course. In the course, we focus on learning Amazon Web Services by actually using it to host, secure, and deliver static websites. It's a simple problem, with many solutions, but it's perfect for ramping up your understanding of AWS. I recently added two new bonus chapters to the course that focus on Infrastructure as Code and Continuous Deployment.

Top comments (0)