Deploying a Node.js App on AWS ECS Fargate with Terraform and GitHub Actions

Deploying a Node.js App on AWS ECS Fargate with Terraform and GitHub Actions

ยทPavan Kalyan Meda

Learn how to deploy a containerized Node.js application on AWS ECS Fargate using Terraform for Infrastructure as Code and GitHub Actions for automated CI/CD.

Deploying a Node.js App on AWS ECS Fargate with Terraform and GitHub Actions

Introduction

Modern cloud-native applications benefit from automated infrastructure provisioning and deployment pipelines. By combining Terraform for Infrastructure as Code (IaC), Amazon ECS Fargate for serverless container orchestration, and GitHub Actions for Continuous Deployment (CD), you can build a reliable and repeatable deployment workflow.

In this guide, you'll deploy a simple Node.js "Hello World" application on Amazon ECS Fargate using Terraform to provision AWS resources. You'll also configure a GitHub Actions workflow that automatically builds and deploys the application whenever changes are pushed to the repository.

This approach enables consistent deployments, reduces manual effort, and improves the overall software delivery process.


Prerequisites

Before getting started, ensure you have the following:

RequirementDescription
AWS AccountAn AWS account with permissions to create and manage ECS, IAM, VPC, ECR, and related AWS resources.
GitHub AccountA GitHub account for hosting the application source code and configuring GitHub Actions workflows.
TerraformInstalled locally to provision AWS infrastructure using Infrastructure as Code (IaC).
DockerInstalled locally for building and testing container images.
Node.jsBasic familiarity with Node.js application development, Docker, and Terraform concepts.

Important: Include only the overall architecture diagram showing the deployment workflow (GitHub โ†’ GitHub Actions โ†’ Amazon ECR โ†’ Amazon ECS Fargate). This provides readers with a clear overview of the complete solution.


Understanding Amazon ECS Fargate

What Is AWS Fargate?

Amazon ECS Fargate is a serverless compute engine for running containerized applications on Amazon Elastic Container Service (Amazon ECS) and Amazon Elastic Kubernetes Service (Amazon EKS).

Unlike traditional container deployments, Fargate eliminates the need to provision or manage EC2 instances. AWS automatically handles the underlying infrastructure, allowing you to focus entirely on your application.

Key Benefits

  • No server management
  • Automatic infrastructure provisioning
  • Built-in scalability
  • Pay only for the CPU and memory resources your containers use
  • Native integration with AWS services such as Amazon ECS, Amazon ECR, IAM, CloudWatch, and Application Load Balancer

Fargate is well suited for microservices, REST APIs, web applications, and event-driven workloads.


Understanding Terraform

What Is Terraform?

Terraform is an open-source Infrastructure as Code (IaC) tool developed by HashiCorp.

It enables developers and DevOps engineers to define cloud infrastructure using declarative configuration files rather than manually provisioning resources through a cloud console.

Terraform supports multiple cloud providers, including:

  • Amazon Web Services (AWS)
  • Microsoft Azure
  • Google Cloud Platform (GCP)
  • Oracle Cloud Infrastructure (OCI)
  • VMware
  • Kubernetes

Key Benefits

  • Infrastructure as Code (IaC)
  • Automated infrastructure provisioning
  • Version-controlled infrastructure
  • Repeatable deployments
  • Multi-cloud support
  • Consistent deployment workflows

Using Terraform ensures that infrastructure remains reproducible, scalable, and easy to maintain throughout the application lifecycle.

Step 1: Create a Simple Node.js Application

In this step, you'll create a basic Node.js web application using Express, a lightweight and flexible web framework for building server-side applications.

The application simply responds with "Hello World!", serving as the workload that will later be containerized and deployed to Amazon ECS Fargate.


Project Structure

Your project should have the following structure:

hello-world-app/  
โ”œโ”€โ”€ app.js  
โ”œโ”€โ”€ Dockerfile  
โ”œโ”€โ”€ package.json  
โ””โ”€โ”€ .gitignore  

Important: Include only one screenshot showing the project directory structure. The remaining screenshots can be omitted because the source code below is sufficient.


Create the Application

app.js

This file contains the application's main logic.

const express = require('express');  
const app = express();

const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {  
  res.send('Hello World!');  
});

app.listen(PORT, () => {  
  console.log(`Server is running on port ${PORT}`);  
});  

Explanation

This application performs the following tasks:

  • Imports the Express framework.
  • Creates an Express application instance.
  • Defines a single HTTP GET endpoint (/).
  • Returns Hello World! when accessed.
  • Starts the web server on the port defined by the PORT environment variable or defaults to 3000.

Configure package.json

The package.json file defines the application's metadata, dependencies, and startup commands.

{  
  "name": "hello-world-app",  
  "version": "1.0.0",  
  "description": "Hello World Node.js app",  
  "main": "app.js",  
  "scripts": {  
    "start": "node app.js"  
  },  
  "dependencies": {  
    "express": "^4.17.1"  
  }  
}  

Explanation

The file includes:

  • Application name and version.
  • Project description.
  • Entry point (app.js).
  • Startup command.
  • Required project dependencies.

In this example, Express is the only dependency.


Create the Dockerfile

To deploy the application on Amazon ECS Fargate, package it as a Docker container.

Create a Dockerfile.

FROM node:14

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD ["npm", "start"]  

Explanation

This Dockerfile performs the following steps:

  1. Uses the official Node.js 14 image.
  2. Creates the application working directory.
  3. Copies the package files.
  4. Installs project dependencies.
  5. Copies the application source code.
  6. Exposes port 3000.
  7. Starts the application using npm start.

At this point, the Node.js application is fully containerized and ready for deployment.


Step 2: Configure Terraform for Amazon ECS Fargate

Next, provision the AWS infrastructure using Terraform.

Terraform creates all of the resources required to deploy the application on Amazon ECS Fargate, including:

  • ECS Cluster
  • Task Definition
  • ECS Service
  • IAM Role
  • Application Load Balancer
  • Target Group
  • Listener

Important: Include only one architecture diagram illustrating the Terraform-managed AWS infrastructure. Individual Terraform editor screenshots are unnecessary because the configuration files below are self-explanatory.


variables.tf

Define the variables used throughout the Terraform configuration.

variable "region" {  
  description = "The AWS region to create resources in"  
  type        = string  
  default     = "ap-south-1"  
}

variable "ecs_cluster_name" {  
  description = "The name of the ECS cluster"  
  type        = string  
}

variable "app_name" {  
  description = "The name of the application"  
  type        = string  
}

variable "vpc_id" {  
  description = "The ID of the VPC where resources will be created"  
  type        = string  
}

variable "subnet_ids" {  
  description = "A list of subnet IDs for the ECS service"  
  type        = list(string)  
}  

These variables make the Terraform configuration reusable across multiple environments.


main.tf

The main.tf file provisions the complete AWS infrastructure required for the application.

It creates:

  • Amazon ECS Cluster
  • IAM Execution Role
  • ECS Task Definition
  • ECS Service
  • Application Load Balancer
  • Target Group
  • Listener
terraform {  
  backend "s3" {  
    bucket = "pavan-your-terraform-state-bucket"  
    key    = "ecs/hello-world-app/terraform.tfstate"  
    region = "ap-south-1"  
  }  
}

provider "aws" {  
  region = var.region  
}

resource "aws_ecs_cluster" "cluster" {  
  name = var.ecs_cluster_name  
}

resource "aws_iam_role" "ecs_task_execution_role" {  
  name = "ecsTaskExecutionRole"

  assume_role_policy = jsonencode({  
    Version = "2012-10-17"

    Statement = [{  
      Effect = "Allow"

      Principal = {  
        Service = "ecs-tasks.amazonaws.com"  
      }

      Action = "sts:AssumeRole"  
    }]  
  })

  managed_policy_arns = [  
    "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"  
  ]  
}

resource "aws_ecs_task_definition" "task" {

  family             = var.app_name  
  network_mode       = "awsvpc"  
  cpu                = "256"  
  memory             = "512"

  execution_role_arn = aws_iam_role.ecs_task_execution_role.arn

  container_definitions = jsonencode([  
    {  
      name      = var.app_name  
      image     = "567738737859.dkr.ecr.ap-south-1.amazonaws.com/hello-world-app:latest"  
      essential = true

      portMappings = [{  
        containerPort = 3000  
        hostPort      = 3000  
      }]  
    }  
  ])  
}

resource "aws_ecs_service" "service" {

  name            = var.app_name  
  cluster         = aws_ecs_cluster.cluster.id  
  task_definition = aws_ecs_task_definition.task.arn

  desired_count = 1  
  launch_type   = "FARGATE"

  network_configuration {

    subnets          = var.subnet_ids  
    assign_public_ip = true

    security_groups = [  
      "sg-098d8e07dc8df4f85"  
    ]  
  }

  load_balancer {

    target_group_arn = aws_lb_target_group.app_target_group.arn  
    container_name   = var.app_name  
    container_port   = 3000  
  }  
}  

The remaining Application Load Balancer, Target Group, and Listener resources can remain unchanged from the original configuration.


terraform.tfvars

Provide the environment-specific values.

Replace the placeholders with values from your AWS environment.

region           = "ap-south-1"

ecs_cluster_name = "cluster-name"

app_name         = "app_name"

vpc_id           = "vpc-id"

subnet_ids = [  
  "subnet-id-1",  
  "subnet-id-2"  
]  

The terraform.tfvars file separates configuration values from infrastructure code, making deployments easier to manage across development, staging, and production environments.

Step 3: Configure GitHub Actions for Continuous Deployment

GitHub Actions is GitHub's built-in Continuous Integration and Continuous Deployment (CI/CD) platform that automates software workflows directly from your repository.

In this project, GitHub Actions automates the deployment process by:

  • Building the Docker image
  • Pushing the image to Amazon Elastic Container Registry (Amazon ECR)
  • Provisioning infrastructure with Terraform
  • Deploying the application to Amazon ECS Fargate

Whenever code is pushed to the main branch, the deployment workflow is automatically triggered.

Important: Include only one architecture diagram illustrating the GitHub Actions workflow (GitHub โ†’ GitHub Actions โ†’ Amazon ECR โ†’ Terraform โ†’ Amazon ECS Fargate). Individual GitHub interface screenshots are unnecessary.


Create the Workflow Directory

Inside the project root, create the following directory structure:

.github/  
โ””โ”€โ”€ workflows/  
    โ””โ”€โ”€ deploy.yml  

deploy.yml

Create a workflow file named deploy.yml.

name: Deploy to ECS

on:  
  push:  
    branches:  
      - main

jobs:

  build:

    runs-on: ubuntu-latest

    steps:

      - name: Checkout code  
        uses: actions/checkout@v2

      - name: Set up Docker Buildx  
        uses: docker/setup-buildx-action@v1

      - name: Configure AWS credentials  
        uses: aws-actions/configure-aws-credentials@v1

        with:  
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}  
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}  
          aws-region: ${{ secrets.AWS_DEFAULT_REGION }}

      - name: Log in to Amazon ECR  
        id: ecr-login  
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push Docker image

        env:  
          ECR_REGISTRY: 567738737859.dkr.ecr.ap-south-1.amazonaws.com  
          ECR_REPOSITORY: hello-world-app  
          IMAGE_TAG: latest

        run: |  
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .  
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

      - name: Install Terraform  
        uses: hashicorp/setup-terraform@v1

        with:  
          terraform_version: 1.0.0

      - name: Terraform Init  
        run: terraform init  
        working-directory: ./terraform

      - name: Terraform Apply  
        run: terraform apply -auto-approve  
        working-directory: ./terraform  

Note: In a production environment, avoid running terraform destroy as part of the deployment workflow unless it is specifically intended for temporary infrastructure.


Workflow Overview

The GitHub Actions workflow performs the following tasks:

  1. Checks out the latest application source code.
  2. Configures Docker Buildx.
  3. Authenticates with AWS.
  4. Logs in to Amazon ECR.
  5. Builds the Docker image.
  6. Pushes the image to Amazon ECR.
  7. Initializes Terraform.
  8. Applies the Terraform configuration to provision or update the infrastructure.

Step 4: Create an Amazon ECR Repository

Amazon Elastic Container Registry (Amazon ECR) is AWS's managed container image registry.

Before deploying the application, create an ECR repository to store the Docker image.

Create the Repository

Using the AWS CLI:

aws ecr create-repository \  
--repository-name hello-world-app  

After the repository is created, Amazon ECR returns details including:

  • Repository URI
  • Repository ARN
  • Registry ID
  • Creation timestamp

The repository URI will later be used by GitHub Actions to push Docker images.


Push Docker Images

The GitHub Actions workflow created earlier automatically performs the following operations:

  • Builds the Docker image.
  • Tags the image.
  • Authenticates with Amazon ECR.
  • Pushes the image to the repository.

No additional manual Docker commands are required after the workflow has been configured.


Step 5: Configure GitHub Secrets

AWS credentials should never be stored directly in source code.

Instead, GitHub provides encrypted repository secrets that are securely accessed during workflow execution.

Navigate to:

Repository โ†’ Settings โ†’ Secrets and variables โ†’ Actions

Create the following secrets:

SecretDescription
AWS_ACCESS_KEY_IDIAM Access Key
AWS_SECRET_ACCESS_KEYIAM Secret Access Key
AWS_DEFAULT_REGIONAWS deployment region

The workflow automatically retrieves these values during deployment.

This approach improves security by preventing sensitive credentials from being committed to the repository.


Step 6: Push the Project to GitHub

Initialize the Repository

cd hello-world-app

git init

git remote add origin \  
https://github.com/your-username/hello-world-app.git

git add .

git commit -m "Initial commit: Node.js app, Dockerfile, and Terraform configuration"

git branch -M main

git push -u origin main  

Add the GitHub Actions Workflow

Create the workflow directory.

mkdir -p .github/workflows  

Create the deployment workflow.

cat \<\<EOF > .github/workflows/deploy.yml

# Add the GitHub Actions workflow content here

EOF  

Commit the workflow.

git add .github/workflows/deploy.yml

git commit -m "Add GitHub Actions workflow for ECS deployment"

git push  

What Happens Next?

After the code is pushed to the main branch:

  1. GitHub Actions automatically starts the workflow.
  2. Docker builds the application image.
  3. The image is pushed to Amazon ECR.
  4. Terraform provisions or updates the AWS infrastructure.
  5. Amazon ECS Fargate launches the latest containerized application.

At this point, the entire deployment processโ€”from source code commit to application deploymentโ€”is fully automated using GitHub Actions, Terraform, Docker, Amazon ECR, and Amazon ECS Fargate.

Step 7: Configure the Terraform Backend

For production deployments, it's recommended to store the Terraform state file in a remote backend instead of keeping it locally. A remote backend enables teams to collaborate safely while maintaining a single source of truth for the infrastructure state.


Why Use a Remote Backend?

Using an Amazon S3 backend provides several advantages:

  • Consistent State โ€“ Ensures all team members use the same Terraform state file.
  • State Locking โ€“ Prevents multiple users from modifying infrastructure simultaneously when combined with DynamoDB state locking.
  • Backup and Versioning โ€“ Amazon S3 supports object versioning for state recovery.
  • Improved Security โ€“ Access to the state file can be controlled using AWS IAM policies.

Configure an Amazon S3 Backend

Update the main.tf file to configure Terraform to store its state remotely.

terraform {

  backend "s3" {

    bucket = "pavan-your-terraform-state-bucket"

    key    = "ecs/hello-world-app/terraform.tfstate"

    region = "ap-south-1"  
  }

}  

Replace the bucket name with your own Amazon S3 bucket before initializing Terraform.


Understanding terraform.tfstate

The terraform.tfstate file records the current state of the infrastructure managed by Terraform.

Terraform automatically creates and updates this file whenever commands such as the following are executed:

terraform init

terraform plan

terraform apply  

Because the backend is configured to use Amazon S3, the state file is stored remotely at:

Bucket:  
pavan-your-terraform-state-bucket

Object:  
ecs/hello-world-app/terraform.tfstate  

This eliminates the need to manually manage the state file while improving collaboration and disaster recovery.


Step 8: Deploy the Application

With the application, Docker image, Terraform configuration, Amazon ECR repository, and GitHub Actions workflow in place, you're ready to deploy the application to Amazon ECS Fargate.


Verify the Configuration

Before deployment, confirm that the following components have been configured correctly:

  • Node.js application
  • Dockerfile
  • Terraform configuration
  • Amazon ECR repository
  • GitHub Actions workflow
  • GitHub Secrets
  • AWS credentials
  • Terraform backend (optional but recommended)

Push the Latest Changes

Commit your changes locally.

git add .

git commit -m "Deploy Node.js application to Amazon ECS Fargate"

git push origin main  

Pushing to the main branch automatically triggers the GitHub Actions workflow.


Monitor the GitHub Actions Workflow

Open your GitHub repository.

Navigate to:

Actions โ†’ Deploy to ECS

The workflow automatically executes the deployment pipeline.

During execution, GitHub Actions performs the following steps:

  1. Checks out the source code.
  2. Builds the Docker image.
  3. Pushes the image to Amazon ECR.
  4. Initializes Terraform.
  5. Applies the Terraform configuration.
  6. Updates the Amazon ECS Fargate service.

Review the Workflow Logs

GitHub Actions provides real-time logs for every stage.

Review the logs to verify:

  • Docker image build
  • Amazon ECR authentication
  • Docker image push
  • Terraform execution
  • ECS deployment

Any warnings or errors can be investigated directly from the workflow logs.


Verify the Deployment

After the workflow completes successfully:

  • Open the Amazon ECS Console.
  • Verify that the ECS Service is running.
  • Confirm that the task status is Running.
  • Open the Application Load Balancer DNS name (or application endpoint) in a browser.

If everything has been configured correctly, the Node.js application should be accessible.

Important: Include a single screenshot showing the successful GitHub Actions workflow or the deployed application running through the ECS Load Balancer. This final verification image is sufficient for this section.




Conclusion

In this guide, you successfully automated the deployment of a Node.js application to Amazon ECS Fargate using Terraform and GitHub Actions.

The solution combines modern DevOps practices to create a reliable and repeatable deployment pipeline.

Key Benefits

  • GitHub Actions automates the build and deployment workflow whenever code is pushed to the repository.
  • Terraform provisions and manages AWS infrastructure using Infrastructure as Code (IaC), ensuring consistency across environments.
  • Amazon ECS Fargate removes the need to manage servers while providing scalable container orchestration.
  • Amazon ECR securely stores and manages Docker container images.

By integrating these technologies, you establish a production-ready deployment pipeline that is scalable, secure, and easy to maintain, providing a strong foundation for deploying modern cloud-native applications on AWS.

Hi! I'm ERICA. Ask me anything!