DEV Community

Cover image for Next.js Deployment using ECS with Fargate
RedRobot.dev
RedRobot.dev

Posted on • Originally published at redrobot.dev

Next.js Deployment using ECS with Fargate

In this post, we'll explore how to deploy a Next.js application to AWS using Elastic Container Service (ECS). We'll cover two approaches: first, using the AWS Management Console, and then using AWS CDK (Cloud Development Kit). Throughout this process, you'll gain a comprehensive understanding of ECS and its key concepts.

Original Post

Elastic Container Services

Amazon Elastic Container Service (ECS) is a powerful container orchestration service that simplifies the process of deploying, managing, and scaling containerized applications. With ECS, you can upload your Docker image and run it in a highly scalable, high-performance, and resilient environment that automatically handles load balancing and auto-scaling for you.
ECS integrates seamlessly with other AWS services such as Application Load Balancer (ALB) and Virtual Private Cloud (VPC) to provide the scalability and load balancing capabilities your application requires. As a comprehensive container orchestration solution, ECS offers the following key features:

  • Auto-scaling: Automatically adjusts the number of containers based on demand
  • Load balancing: Distributes incoming traffic across multiple containers
  • IAM integration: Manages access control and permissions
  • Networking: Provides flexible networking options through VPC integration
  • Logging: Offers built-in logging capabilities for easy troubleshooting
  • Monitoring: Enables real-time monitoring of your containerized applications

This robust set of features makes ECS an excellent choice for running and scaling Docker containers on AWS, providing you with a reliable and efficient platform for your containerized applications.

Before diving into ECS, it's important to understand two key concepts:

  1. Virtual Private Cloud (VPC): If you're not familiar with VPC, we recommend reading our "AWS Virtual Private Network Explained" post. This will provide you with the necessary background information to effectively use ECS.

  2. Containerization: To use ECS, we need to containerize our Next.js application using Docker. If you're new to Docker and containers, our "Next.js Containerization" post explains what containers are and how to containerize and run a Next.js app.

Additionally, it's worth noting that an Application Load Balancer (ALB) is typically used when you have more than one task and need to route packets for high-availability deployments. The ALB distributes incoming application traffic across multiple targets, such as EC2 instances, in multiple Availability Zones.

Architecture

This is the typical ECS deployment, and in this post we will be creating the following architecture.

ECS Architecture

Don't worry if this architecture seems complex at first glance—we'll guide you through creating it without delving into low-level details. Here's a simplified overview of how it works:

  1. Clients send HTML page requests through the Internet Gateway.
  2. These requests are received by an Application Load Balancer (ALB).
  3. The ALB forwards the requests to one of the Next.js tasks.
  4. These tasks are managed by AWS Fargate and Elastic Container Service (ECS).
  5. During deployment, the Next.js image is loaded from Elastic Container Registry (ECR).

This setup ensures efficient distribution of incoming traffic and seamless management of your containerized Next.js application.

Using Console

Upload image to AWS

First step is to build and upload our container image to AWS ECR or the Elastic Container Registry.

Follow the steps described here to build and run a docker container of a Next.js application.

To verify you have correctly built the image, run the following command and verify you see a similar output

$ docker image ls
REPOSITORY                  TAG       IMAGE ID       CREATED      SIZE
nextjs-frontend             latest    f42f21949703   4 days ago   738MB
Enter fullscreen mode Exit fullscreen mode

Create a Register in the ECR console. Open the AWS Console and go to the ECS service page. From the left panel, click Repositories, then create Create repository.

ECR Service Page

Select private for the visibility settings, and type in nextjs-frontend for the repository name.

Now lets upload the image to ECR, first you need to get an authentication token for each registry. run this to do that:

replace the two "<region>" and the one "<aws_account_id>" values with yours.

aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin <aws_account_id>.dkr.ecr.<region>.amazonaws.com
Enter fullscreen mode Exit fullscreen mode

so it should look something like this:

aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 975050147627.dkr.ecr.us-east-1.amazonaws.com
Enter fullscreen mode Exit fullscreen mode

then tag your local image, with the name of the remote repository name, replace the command with the correct region and account id:

docker tag nextjs-frontend <aws_account_id>.dkr.ecr.<region>.amazonaws.com/nextjs-frontend:lastest
Enter fullscreen mode Exit fullscreen mode

so something like this:

docker tag nextjs-frontend 975050147627.dkr.ecr.us-east-1.amazonaws.com/nextjs-frontend:lastest
Enter fullscreen mode Exit fullscreen mode

to verify run docker image ls and you should see a secondary image with the name you entered above.

to finally push your image run:

docker push <aws_account_id>.dkr.ecr.<region>.amazonaws.com/nextjs-frontend:lastest
Enter fullscreen mode Exit fullscreen mode

so something like this:

docker push 975050147627.dkr.ecr.us-east-1.amazonaws.com/nextjs-frontend:lastest
Enter fullscreen mode Exit fullscreen mode

once pushed, you should see the following output:

The push refers to repository [975050147627.dkr.ecr.us-east-1.amazonaws.com/nextjs-frontend]
947f16d41eae: Pushed
df7925d45941: Pushed
f0d00ab152e9: Pushed
5e60551565b3: Pushed
f7561880256e: Pushed
60fb86bbe6c7: Pushed
dc01ec349cf7: Pushed
f87675c73d4d: Pushed
28576ee1ff32: Pushed
daa732f7b271: Pushed
7cc8f366b357: Pushed
78561cef0761: Pushed
latest: digest: sha256:4143d5b35602520f3f22b84cd3e15b90c6bbe1d315a44ad7b19a93791eb41266 size: 2821
Enter fullscreen mode Exit fullscreen mode

You can read the "Pushing a Docker image to an Amazon ECR private repository" article from AWS for more detailed explanation or if you are having trouble with the steps.

Defining a VPC

Follow the instructions in AWS Virtual Private Network Explained to create a default VPC using the VPC and more option. Name your vpc something like nextjs-vpc. By the end you should have a VPC with two AZ's, with one public and one private subnet per AZ, with 3 routing tables, one connected to a IG and two isolating the private subnets, and we don't need a VPC endpoint for this.

If you are new to Virtual Private Cloud (VPC) read AWS Virtual Private Network Explained and follow the instructions. IF you are familair with VPC, create a VPC and use the "VPC and more" option to create a default VPC. Name your VPC something descriptive, such as "nextjs-vpc".

After completing these steps, your VPC should have the following configuration:

  • Two Availability Zones (AZs)
  • One public subnet and one private subnet per AZ
  • Three routing tables, One connected to an Internet Gateway (IG), Two isolating the private subnets
  • No VPC endpoint (not needed for this setup)

This configuration provides a secure and scalable network foundation for your ECS deployment.

Defining a Task Definition

To prepare your application to run on Amazon ECS, you need to create a task definition.
Task definitions specify various parameters for your application such as:

  • Containers to use (up to a maximum of 10 that form your application)
  • Launch type to use (such as Fargate or EC2)
  • Inbound ports that need to be opened for your application
  • Data volumes to be used with the containers in the task
  • Additional launch type-specific parameters

Task definitions act as a blueprint for your application, defining how it should run within ECS. They allow you to specify resource requirements, network configurations, and other crucial settings that determine how your containerized application will operate.

Here is an example of a task definition containing a single container that runs an NGINX web server using the
the Fargate launch type.

{
    "family": "webserver",
    "containerDefinitions": [
        {
            "name": "web",
            "image": "nginx",
            "memory": "100",
            "cpu": "99"
        }
    ],
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "networkMode": "awsvpc",
    "memory": "512",
    "cpu": "256"
}
Enter fullscreen mode Exit fullscreen mode

Lets define a task definition. Go the ECS Service page, from the left panel select Task definition.

ecs-task-definition-list

Click on the Create new task definition, then select the Create new task definition so we can explore some of the properties.

Note: One of the reason why it's important to know how to use the AWS console is to explore properties and options surrounding services and also learning how to debug the configuration settings.

For the Task definition family enter a name such as nextjs-frontend-task.
The next section is the Infrastructure requirements section. For this section select AWS Fargate for the Launch type.

Select Linux/x86_64 for the operating system/architecture.
For the Task size select, a CPU size of 0.5vCPU and 1GB for the Memory size.
Set the Task role to None.

task-definition-infrastructure

AWS Fargate is a serverless compute engine for containers that works with Amazon ECS. It offers several key advantages:

Serverless Operation: Fargate allows you to run containers without managing servers or clusters.
Simplified Management: You no longer need to provision, configure, or scale clusters of virtual machines to run your containers.
Reduced Complexity: Fargate eliminates the need to:

  • Choose server types
  • Decide when to scale your clusters
  • Optimize cluster packing

Focus on Applications: With infrastructure management handled by Fargate, you can concentrate on designing and building your applications.

In essence, Fargate abstracts away much of the underlying infrastructure complexity, allowing you to deploy and run containerized applications more easily and efficiently on Amazon ECS.

For the Container - 1 section, specify something like nextjs-frontend for the name, and the Image URI is the ECR docker image URI that we used earlier. So it should be something like this:

<aws_account_id>.dkr.ecr.<region>.amazonaws.com/nextjs-frontend:lastest
Enter fullscreen mode Exit fullscreen mode

you can find this URL from the ECR Service page, by clicking on the Repository, and the nextjs-frontend image name. The following page will be displayed with the URI value in the Details section. Copy the value by clicking on the double square icon:

ecr-repository-details

Then add the value to the Image URI value of the ECS page.
For the Port mappings specify port 3000 as that is our default Next.js port number.

Don't specify anything for the Resource allocation limits.

Task Definition Container

Explore the remaining optional configurations, but don't change any settings.
Click Create to finish.

Create Cluster

A Logical grouping of tasks or services.
The services can include, EC2 launch types, containers, capacity providers, fargate launch types.
What is it for?

A regional grouping of one or more container instances where you can run task requests.
Each account receives a default cluster the first time you use the Amazon ECS service,
but you may also create other clusters. Clusters may contain more than one instance
type simultaneously.

Now lets create a cluster, from the left panel click Clusters, then click on Create cluster.
In the cluster create view, specify the cluster name nextjs-frontend-cluster and select AWS Fargate (serverless) for the infrastructure.

cluster-configuration

Click Create to finish.

Create Task

So we have our Task Definition, our cluster now we need to create a task. First, lets create a single task and run it, then check to verify the container is working. Then we

Click on the cluster created in the last step nextjs-frontend-cluster

cluster-frontend-details

Select the Tasks tab on the bottom, and click on Run new task.
In the Create view go to the Deployment configuration section

for the Family value, specify the task definition we specified earlier and set the number of desired tasks to 1.

task-deployment-configuration

In the Networking section, select the VPC nextjs-vpc created in the first part.
Clear the current subnet select, and from the new list, select to add the two public subnets.

For the security group, create a new group and set port 3000 to be opened. You can name the security group port-3000 for easy reading.

Leave the Public IP enabled.

ecs-networking

Click Create and in the next screen, wait until the Last status value changes to Running. Once Ready, click on the Task to view the details

task-running-state

From the task details view, in the Networking tab, copy the public IP

task-networking-details

Open a new tab, and paste the IP, appending the port number :3000 at the end.
You should see the Next.js landing page

Website

We verified that our Task definition works and that if we deploy a task, the page loads and works fine.
Now select the task from the cluster details view, and click Stop to destroy the task. In the next step we are going to create a service that will automatically create multiple tasks.

Create Service with a Load Balancer

Now that we verified that the task is working, we can move on and create a service. A Service defines or encapsulates multiple tasks and it's typically created with a load balancer to route requests to different tasks. In addition to encapsulating tasks, the Service has a scheduler which maintains a desired count of tasks, so if a task fails and exits, or the health check is not responding, it will launch a new task and route traffic to the new task.

To create a Service, go to the Cluster details view and select the Services tab this time instead of the Tasks tab.
Click Create and you will see a similar view to the Task creation page.

Scroll down to the Deployment configuration section and make sure the Application type is Service. For the Family select the same task as before nextjs-frontend-task, and set the service name to nextjs-frontend-service. The set the Desired tasks count to 2.

service-deployment-config

In the Networking section, select the VPC nextjs-vpc created in the first part. Clear the current subnet select, and from the new list, select the two public subnets. Also make sure to add the port-3000 and port-80 to the security group. Also turn on the Public IP option if not already enabled.

We can stop here and click Create, but there is no point for a Service without a load balancer, especially for tasks that are not accessible.
Scroll down to the Load balancing optional section.

For the Load balancer type select Application load balancer. The container, should get automatically populated with nextjs-frontend 3000:3000. Under the application load balancer. Select the Create a new load balancer and for the load balancer name Type in nextjsalb. On the bottom, where it says listener make sure that create new listener options is selected and the port is set to 80 with the protocol as HTTP. And for the target group, make sure the create new target group is selected and leave all values as is. The protocol should say HTTP And the health check protocol should say also HTTP with the health check path set to by default to "/".

Click create. This will take a bit longer since it's creating several resources.
Once done, open the clusters services tab select the service we just created, it should have the name nextjs-frontend-service. Then on the top, select the configure and networking tab And scroll down to the network configurations section. On the right side of that section you will see a DNS value. You can click on open address or copy the address and paste it in a new tab.

alb-dns-name

If you follow the instructions correctly, you will see the Nextjs default page load. The difference between this method and the method of deploying one task is that now we have a load balancer that is listening on port 80 and it is routing traffic to two separate tasks or containers that are running our nextjs app, and it will use A routing strategy to forward traffic to either task.

Using AWS CDK

By leveraging IaC, you can automate and replicate all the setup and configuration work typically done through the console. This approach significantly speeds up your website deployment process.

Lets automate all of the steps we took using AWS CDK, which is an IaC or Infrastructure as code framework. All the steps we performed manually it can be automated and also we can add logic as well so based on some condition, ie if it's a production deployment we can add a few other steps vs if it's a test deployment we can skip a few other steps.

Prerequisites

  • An AWS account
  • Node.js and npm installed
  • AWS cli and cdk setup and configured
  • Basic knowledge in Javascript and Typescript

Setup, Code and Deploy

  1. Initialize a new CDK project:
mkdir temp-project && cd temp-project
mkdir infrastructure && cd infrastructure
cdk init app --language typescript
Enter fullscreen mode Exit fullscreen mode
  1. Replace the content of the file lib/infrastructure-stack.ts with:
import * as cdk from "aws-cdk-lib"
import * as ecs from "aws-cdk-lib/aws-ecs"
import { Construct } from "constructs"
import { IpAddresses, Vpc } from "aws-cdk-lib/aws-ec2"
import { ApplicationLoadBalancedFargateService } from "aws-cdk-lib/aws-ecs-patterns"
import { ApplicationProtocol } from "aws-cdk-lib/aws-elasticloadbalancingv2"

export class InfrastructureStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)
    const vpc = new Vpc(this, `${id}-vpc`, {
      vpcName: `${id}-vpc`,
      ipAddresses: IpAddresses.cidr("10.0.0.0/16"),
      maxAzs: 2,
    })

    const cluster = new ecs.Cluster(this, `${id}-cluster`, {
      clusterName: `${id}-cluster`,
      vpc,
    })

    const fargateService = new ApplicationLoadBalancedFargateService(
      this,
      `${id}-fargate`,
      {
        serviceName: `${id}-fargate-service`,
        loadBalancerName: `${id}-fargate-lba`,
        cluster,
        cpu: 512,
        memoryLimitMiB: 1024,
        protocol: ApplicationProtocol.HTTP,
        desiredCount: 1,
        publicLoadBalancer: true,
        taskImageOptions: {
          image: ecs.ContainerImage.fromAsset("../nextjs-frontend"),
          containerName: `${id}-container`,
          containerPort: 3000,
        },
      }
    )

    // Output the load balancer URL
    new cdk.CfnOutput(this, `${id}-url`, {
      value: fargateService.loadBalancer.loadBalancerDnsName,
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

now deploy the stack

cdk deploy
Enter fullscreen mode Exit fullscreen mode
  1. Once deployment, you will see a message in the console that has the load balancers DNS name. Copy paste the value and paste it in a browser and you should see the same Next.js landing page.

  2. And to destroy all the resources just run

cdk destroy
Enter fullscreen mode Exit fullscreen mode
  1. And enter y to confirm you want to delete. And that is it! all resource will been destroyed according to the dependency tree CDK keeps track of.

Conclusion

This guide has walked through the process of deploying a Next.js application to AWS using Elastic Container Service (ECS), demonstrating both manual deployment via the AWS Console and automated deployment using AWS CDK. By containerizing the Next.js app and leveraging ECS, developers can create a scalable, high-performance infrastructure that takes advantage of features like auto-scaling and load balancing. The manual process provides a deep understanding of the components involved - from ECR for image storage to task definitions, clusters, and services - while the CDK approach showcases how Infrastructure as Code can streamline and automate the entire deployment process. Whether you choose the hands-on console method or the efficient CDK route, this guide equips you with the knowledge to deploy and manage containerized Next.js applications on AWS, setting the foundation for robust, cloud-native web applications.

Interested in learning more?

If you are interested in creating a project from scratch using AWS CDK and serverless technologies, you can check out the course linked below. In this course, we go over each service in detail, explaining what they do, and actually create a dynamic application with user login and authentication.

Serverless Fullstack with AWS/CDK/NextJS & Typescript

Resources

AWS official ECS workshop has a great walkthrough
https://ecsworkshop.com/introduction/ecs_basics/

Top comments (0)