Introduction
In this blog, we will explore how to deploy a NestJS application to Amazon ECS (Elastic Container Service) using GitHub Actions for a seamless CI/CD pipeline. We’ll leverage several AWS services, including Application Load Balancer (ALB), Elastic Container Registry (ECR), ECS, ElastiCache for Redis, IAM, EC2, S3, and VPC, to create a microservice architecture.
Please Note: In the blog, I have only considered the deployment to ECS and the CICD security aspects. For managing environmental secrets securely, please ensure you use AWS Secrets Manager or SSM Parameter Store to manage and retrieve sensitive information, such as database credentials and API keys, within your ECS tasks and pipeline.
Prerequisites
Before getting started, ensure you have the following:
- An AWS account
- Basic knowledge of Docker, GitHub Actions, and AWS services
- A pre-configured NestJS application with the structure detailed below
- Terraform installed locally
GitHub URL of the NestJS application can be found here, and the Terraform scripts can be found here.
Containerizing the NestJS Application
The first step is to containerize the NestJS application using Docker. Below is a sample Dockerfile that you can use:
FROM node:lts-alpine3.19
# Create and set the working directory inside the container
WORKDIR /app
# Copy package.json and package-lock.json files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code to the working directory
COPY . .
# Build the application
RUN npm run build
# Expose port and start application
EXPOSE 3000
CMD [ "node", "dist/main.js" ]
This Dockerfile sets up the environment, installs dependencies, builds the NestJS application, and exposes port 3000, which is the default port for the application.
Create an ECS Cluster
Navigate to the Amazon ECS Console
- Go to the Amazon ECS console.
Create a New Cluster:
- Click on "Create Cluster".
Configure Cluster Settings:
- Cluster Name:
- Enter a name for your cluster. For example, nestjs-ecs-cluster.
- The cluster name must be 1 to 255 characters long and can include a-z, A-Z, 0-9, hyphens (-), and underscores (_).
Infrastructure:
- Select "Customized" to manually configure the cluster's infrastructure.
Configure Network Settings
VPC:
- Select the VPC in which your ECS cluster will be deployed. If you don't have a specific VPC, you can use the default VPC or create a new one.
Subnets:
- Select the subnets where your EC2 instances will be launched and where your tasks will run. It's recommended to use multiple subnets across different Availability Zones for redundancy.
Security Group:
- Choose an Existing Security Group:
- Select an existing security group that has the necessary inbound and outbound rules for your application.
Create a New Security Group:
- Alternatively, create a new security group and configure the rules to allow traffic on the required ports, such as port 3000 for your NestJS application.
Auto-assign Public IP:
- Set the Auto-assign public IP option to "Use subnet setting" or choose to auto-assign a public IP if your instances need to be accessible over the internet.
Review and Create the Cluster
Review Configuration:
- Review all the settings you have configured for the cluster, EC2 instances, networking, and security.
Create Cluster:
- Click "Create" to start the creation of your ECS cluster with the configured settings.
- Wait for Cluster Creation
The creation process may take a few minutes. Once complete, you will have an ECS cluster set up with EC2 instances ready to run your tasks.
Register Task Definitions
- Once your cluster is created, you can proceed to register task definitions that will run on the EC2 instances in this cluster.
Create an ECR Repository
Create a New Repository:
- Click on "Create Repository".
- Enter a name for your repository, such as nestjs-app-repo.
- Leave the defaults for other settings unless you have specific requirements.
- Click "Create Repository".
Push Your Docker Image to ECR:
- After creating the repository, follow the steps provided by ECR to push your Docker image. This typically includes:
- Authenticating Docker to the ECR registry.
- Building your Docker image.
- Tagging the image with your ECR repository URI.
- Pushing the image to ECR.
Example commands:
Retrieve an authentication token and authenticate your Docker client to your registry. Use the AWS CLI:
aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/d3k6a8t8
Build your Docker image using the following command.
docker build -t nestjs-ecs-cluster/nestjs-app .
After the build completes, tag your image so you can push the image to this repository:
docker tag nestjs-ecs-cluster/nestjs-app:latest public.ecr.aws/d3k6a8t8/nestjs-ecs-cluster/nestjs-app:latest
Run the following command to push this image to your newly created AWS repository:
docker push public.ecr.aws/d3k6a8t8/nestjs-ecs-cluster/nestjs-app:latest
Creating the Task Definition
After your Docker image is available in ECR, proceed to create the task definition in the ECS console:
Create a Task Definition:
- Select "Task Definitions" from the side menu and click on "Create new Task Definition".
Choose Launch Type Compatibility:
- Select the launch type (EC2 or Fargate) depending on your ECS cluster configuration.
Configure Container Definitions:
- Add a container definition that points to the Docker image in your ECR repository.
- Set up port mappings, environment variables, resource limits, and any other required settings.
For this application, we need the following environment variables:
PORT=3000
MONGODB_URI=mongodb://localhost:27017/test
REDIS_HOST=localhost
REDIS_PORT=6379"
Save and Use the Task Definition:
- Save the task definition and use it in your ECS service to deploy your NestJS application.
Creating an ECS Service
Create a New Service:
- Click on "Create" under the Services tab in your cluster.
Compute Configuration:
Compute options:
- Select "Capacity provider strategy" as the Compute options if you're using EC2 instances for the service.
Task Definition:
- Select the task definition you previously created that references your Docker image in ECR.
Service Name:
Enter a name for your service, such as nestjs-service.
Number of Tasks:Specify the desired number of tasks to run simultaneously. For example, you can set this to 2.
Configure the Load Balancer (Optional but Recommended):
- If you want to distribute traffic across your tasks, it's recommended to use an Application Load Balancer (ALB).
Load Balancing Type:
- Select "Application Load Balancer".
Load Balancer Name:
Choose your existing ALB or create a new one if needed.
Container to Load Balance:Specify the container that will receive traffic from the load balancer. Choose the container port that you exposed in the task definition (e.g., port 3000 for your NestJS application).
Target Group:Create a new target group or select an existing one for your ECS tasks. This target group will be used to route traffic to the running tasks.
Health Check Path:
- Specify the health check path that the ALB will use to monitor the health of your tasks. For example, if you have a /health endpoint in your NestJS app, set the health check path to /health.
Review and Create:
- Review the configuration of your ECS service, including task definition, load balancing, and auto-scaling settings.
- Click "Create Service" to deploy your service.
Transitioning to Automated Deployment
- Up until now, we have been deploying the application using the AWS Management Console manually. Now, let's focus on automation. The Terraform script will create all the necessary AWS resources for this application, and we will use GitHub Actions to automate the deployment process.
Terraform directory structure:
.
├── backend.tf
├── locals.tf
├── main.tf
├── modules
│ ├── alb
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── ecr
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── ecs
│ │ ├── asg.tf
│ │ ├── ecs.sh
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── elasticache
│ │ ├── main.tf
│ │ ├── output.tf
│ │ └── variables.tf
│ ├── iam
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── mongodb_ec2
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── nsg
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ ├── s3
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ └── variables.tf
│ └── vpc
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
├── outputs.tf
├── providers.tf
├── terraform.tfvars
└── variables.tf
Steps to Create AWS Resources Using Terraform
Clone the Terraform script template:
- Clone the repository containing the Terraform script to your local machine.
Navigate to the Terraform template directory:
- Move into the directory where the Terraform files are located.
Run the following command to initialize the working directory that contains the Terraform configuration files:
terraform init
terraform plan
terraform apply --auto-approve
Consideration for the GitHub Actions Workflow
We need to ensure our ECS task definition is updated during the deployment process. I have created the following:
Please don't forget to update your AWS credentials under Settings > Secrets and Variables > Actions with the required AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
ECS Task Definition File:
- Located at .aws/tps-microservice-td.json This contains the configuration for our ECS task, including environment variables, container configuration, and resource limits.
{
"containerDefinitions": [
{
"name": "tps-container",
"image": "861569119044.dkr.ecr.us-west-2.amazonaws.com/nestify-ecr:33a0d65a43f0b7fb2daa9d15c6ca2a9af3fdbfe3",
"cpu": 0,
"portMappings": [
{
"name": "tps-container-3000-tcp",
"containerPort": 3000,
"hostPort": 3000,
"protocol": "tcp",
"appProtocol": "http"
}
],
"essential": true,
"environment": [
{
"name": "REDIS_PORT",
"value": "6379"
},
{
"name": "PORT",
"value": "3000"
},
{
"name": "MONGODB_URI",
"value": "mongodb://54.68.197.219:27017/test"
},
{
"name": "REDIS_HOST",
"value": "nestify-redis.qtsuer.0001.usw2.cache.amazonaws.com"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/tps-microservice-td",
"awslogs-region": "us-west-2",
"awslogs-stream-prefix": "ecs"
}
}
}
],
"family": "tps-microservice-td",
"taskRoleArn": "arn:aws:iam::861569119044:role/ecsTaskExecutionRole",
"executionRoleArn": "arn:aws:iam::861569119044:role/ecsTaskExecutionRole",
"networkMode": "bridge",
"cpu": "128",
"memory": "128",
"requiresCompatibilities": [
"EC2"
],
"runtimePlatform": {
"cpuArchitecture": "X86_64",
"operatingSystemFamily": "LINUX"
}
}
GitHub Actions Workflow:
- The workflow file is located at .github/workflows/main.yml. This file handles the automation of our deployment process, including updating the ECS task definition when changes are made.
name: Deploy to Amazon ECS
on:
push:
branches:
- master
env:
AWS_REGION: us-west-2
ECR_REPOSITORY: nestify-ecr
ECS_SERVICE: tps-microservice-svc
ECS_CLUSTER: nestify-ecs-cluster
ECS_TASK_DEFINITION: .aws/tps-microservice-td.json
CONTAINER_NAME: tps-container
jobs:
build_and_deploy:
name: Build and Deploy to ECS
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build Docker Image
run: |
docker build -t ${{ env.ECR_REPOSITORY }}:${{ github.sha }} .
- name: Push Docker Image to Amazon ECR
run: |
docker tag ${{ env.ECR_REPOSITORY }}:${{ github.sha }} ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
docker push ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ env.ECS_TASK_DEFINITION }}
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
Once your pipeline completes, check with your ALB (Application Load Balancer) endpoint to verify the deployment.
That's it! You’ve successfully deployed your application on ECS using GitHub Actions and Terraform. 🚀😊
Top comments (0)