DEV Community

Mostafa Dekmak
Mostafa Dekmak

Posted on

A Guide to Deploying Nest.js Applications with AWS CodePipeline and ECS Fargate

Github Link : https://github.com/dkmostafa/dev-samples
AWS-CDK Infrastructure Branch : https://github.com/dkmostafa/dev-samples/tree/infra
NestJs Application Branch : https://github.com/dkmostafa/dev-samples/tree/nestjs-application

Goal :

The goal of deploying a Nest.js application using AWS CodePipeline and AWS ECS Fargate is to establish an efficient and automated deployment pipeline. This process aims to simplify the deployment workflow, ensure consistency across environments, and enhance scalability by utilizing the power of AWS services. By achieving this goal, developers can focus more on coding and less on manual deployment tasks, ultimately leading to faster delivery of updates and improved application reliability.

Introduction :

In the world of modern application development, deploying applications efficiently and reliably is crucial. In this article, we will delve into the process of deploying a Nest.js application using powerful AWS services: AWS CodePipeline and AWS ECS Fargate. These tools, when combined, provide a streamlined and automated deployment pipeline that simplifies the deployment workflow and enhances scalability.

AWS CodePipeline allows us to create automated release pipelines that model the steps required to release our software, from source code to production. It facilitates continuous integration and delivery (CI/CD), automating the build, test, and deployment phases.

AWS ECS (Elastic Container Service) Fargate, on the other hand, offers a fully managed container orchestration service. It allows us to deploy and manage containers without having to manage the underlying infrastructure. With Fargate, we can focus on our application logic while AWS takes care of the container deployment, scaling, and monitoring.

Throughout this article, we will explore how to set up these tools to deploy a Nest.js application seamlessly. We'll dive into the code, discussing the use of TypeScript and AWS CDK (Cloud Development Kit) to define our infrastructure as code. By the end, you'll have a clear understanding of how to leverage AWS CodePipeline and ECS Fargate to automate and scale your Nest.js applications effectively. Let's get started!

Prerequisites :

Before we begin, make sure you have the following:

AWS account with necessary permissions
Node.js installed on your local machine
Basic understanding of NestJs, AWS CDK, and CodePipeline
Install AWS-CDK cli on your local machine : https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html

First Step : Creating an Amazon ECR Repository with AWS CDK

The first step in our deployment process is to create an Amazon ECR (Elastic Container Registry) repository using AWS CDK (Cloud Development Kit). This repository will serve as the container image storage for our Nest.js application.

    private createEcrImage(_props:ICreateEcrImage):Repository{
        const repository: Repository = new Repository(this, _props.id, {
            imageScanOnPush: true,
            repositoryName:_props.name
        });
        return repository;
    }
Enter fullscreen mode Exit fullscreen mode

Second Step : Setting Up AWS CodePipeline for Continuous Integration

After creating our Amazon ECR repository, the next step in deploying our Nest.js application with AWS CodePipeline and ECS Fargate is to establish the continuous integration (CI) pipeline. This pipeline will automate the process of building and packaging our application's code whenever changes are pushed to the repository.

createBuildPipeline(_props: IPipelineConfig, account: string, region: string, repo: Repository) {
    const outputSources: Artifact = new Artifact();
    const outputWebsite: Artifact = new Artifact();

    // Source Action: GitHub Source
    const sourceAction: GitHubSourceAction = new GitHubSourceAction({
        actionName: 'GitHub_Source',
        owner: _props.githubConfig.owner,
        repo: _props.githubConfig.repo,
        oauthToken: SecretValue.secretsManager(_props.githubConfig.oAuthSecretManagerName),
        output: outputSources,
        branch: _props.githubConfig.branch,
        trigger: GitHubTrigger.WEBHOOK
    });

    // CodeBuild Project for Build
    const buildProject = new PipelineProject(this, "BuildWebsite", {
        projectName: "BuildWebsite",
        buildSpec: BuildSpec.fromSourceFilename(_props.buildSpecLocation),
        environment: {
            buildImage: LinuxBuildImage.STANDARD_7_0,
            environmentVariables: {
                AWS_REGION: { value: region },
                AWS_ACCOUNT: { value: account },
                ECR_REPO: { value: repo.repositoryName },
            },
        },
    });

    // Allow CodeBuild to interact with ECR
    buildProject.addToRolePolicy(new PolicyStatement({
        resources: ["*"],
        actions: ['ecr:*'],
        effect: Effect.ALLOW
    }));

    // CodeBuild Action
    const buildAction: CodeBuildAction = new CodeBuildAction({
        actionName: "BuildWebsite",
        project: buildProject,
        input: outputSources,
        outputs: [outputWebsite],
    });

    // Define the Pipeline
    const pipeline: Pipeline = new Pipeline(this, _props.pipelineId, {
        pipelineName: _props.pipelineName,
        stages: [
            {
                stageName: "Source",
                actions: [sourceAction],
            },
            {
                stageName: "Build",
                actions: [buildAction],
            },
        ]
    });

    // Add Manual Approval Stage
    const approveStage = pipeline.addStage({ stageName: 'Approve' });
    const manualApprovalAction = new ManualApprovalAction({
        actionName: 'Approve',
    });
    approveStage.addAction(manualApprovalAction);

    return {
        pipeline: pipeline,
        output: outputSources
    };
}

Enter fullscreen mode Exit fullscreen mode

For our code build , a buildspec.yml file should be defined :

version: 0.2

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws --version
      - aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com
      - REPOSITORY_URI=$AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com/$ECR_REPO
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $REPOSITORY_URI:latest nestjs-app/.
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker images...
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG

Enter fullscreen mode Exit fullscreen mode

Explanation of the Code:
Method Signature:

createBuildPipeline(_props: IPipelineConfig, account: string, region: string, repo: Repository) { ... }
This method takes in the configuration details for the pipeline (_props), AWS account ID (account), AWS region (region), and the ECR repository (repo) as arguments.
Artifact Definition:

const outputSources: Artifact = new Artifact();
const outputWebsite: Artifact = new Artifact();
Artifacts represent the inputs and outputs of the pipeline stages. Here, we create two artifacts: outputSources for source code and outputWebsite for the built application.
Source Action: GitHub Source:

We define a GitHubSourceAction to fetch the source code from a GitHub repository.
owner, repo, oauthToken: GitHub repository details.
output: The artifact where the fetched source code will be stored.
branch: The branch of the GitHub repository to monitor for changes.
CodeBuild Project:

We create a PipelineProject named "BuildWebsite" for building our Nest.js application.
buildSpec: Specifies the build specifications from a file.
buildImage: The Docker image used for the build process.
environmentVariables: Sets environment variables required for the build, including AWS region, account ID, and ECR repository name.
CodeBuild Permissions:

We add a policy to the CodeBuild project allowing it to interact with the ECR repository.
This policy grants necessary permissions for CodeBuild to push the built Docker image to the ECR repository.
CodeBuild Action:

We create a CodeBuildAction named "BuildWebsite" using the buildProject.
This action triggers the build process defined in the buildProject.
It takes outputSources as input and produces outputWebsite.
Pipeline Definition:

We define the Pipeline with the specified stages and actions.
The "Source" stage fetches the source code from GitHub.
The "Build" stage executes the buildProject to build the Nest.js application.
Manual Approval Stage:

We add an "Approve" stage that requires manual approval before proceeding.
This allows for human intervention to ensure control over the deployment process.
Return:

Finally, we return an object containing the created pipeline and the outputSources artifact.
By executing this code, we create an AWS CodePipeline that fetches the source code from a GitHub repository, builds the Nest.js application using CodeBuild, and produces an artifact. The pipeline also includes a manual approval stage for added control over the deployment process.

Now that our CodePipeline is set up, we can move forward to the next step of deploying our Nest.js application to AWS ECS Fargate. Stay tuned for the continuation of our deployment journey!

Third Step : Setting Up AWS ECS Cluster with Fargate Service

Now that we have our CodePipeline ready to build our Nest.js application, the next step is to create an AWS ECS (Elastic Container Service) cluster along with an ECS Fargate service. This will allow us to deploy and manage our containerized application seamlessly.

in this step, we define the createEcs method within our EcsApplicationConstruct class. This method handles the creation of the ECS cluster, Fargate task definition, and the Fargate service.

private createEcs(_props: ICreateEcs, _account: string, _region: string, _ecrName: string) {
    // Create Execution Role for ECS Task
    const executionRole: Role = new Role(this, _props.executionRole.id, {
        assumedBy: new ServicePrincipal("ecs-tasks.amazonaws.com"),
        roleName: _props.executionRole.name
    });

    // Define Permissions for Execution Role
    executionRole.addToPolicy(new PolicyStatement({
        resources: ["*"],
        actions: [
            "ecr:GetAuthorizationToken",
            "ecr:BatchCheckLayerAvailability",
            "ecr:GetDownloadUrlForLayer",
            "ecr:BatchGetImage",
            "logs:CreateLogStream",
            "logs:PutLogEvents"
        ],
        effect: Effect.ALLOW
    }));

    // Define Fargate Task Definition
    const taskDefinition: FargateTaskDefinition = new FargateTaskDefinition(
        this,
        _props.taskDefinitionId,
        {
            executionRole: executionRole,
            runtimePlatform: {
                cpuArchitecture: CpuArchitecture.X86_64,
                operatingSystemFamily: OperatingSystemFamily.LINUX
            },
        },
    );

    // Add Container to Task Definition
    const container = taskDefinition.addContainer(
        _props.containerConfig.id,
        {
            image: ContainerImage.fromRegistry(_ecrName),
            containerName: _props.containerConfig.name,
            essential: true,
            portMappings: [
                {
                    containerPort: 8080,
                    protocol: Protocol.TCP
                },
            ],
            logging: new AwsLogDriver({
                streamPrefix: `${_props.containerConfig.name}-ecs-logs`
            })
        }
    );

    // Create VPC for ECS Cluster
    const vpc = new Vpc(this, `${_props.containerConfig.name}-vpc`, {});

    // Create ECS Cluster
    const cluster: Cluster = new Cluster(this, `${_props.containerConfig.name}-cluster`, {
        clusterName: _props.clusterName,
        vpc
    });

    // Create Application Load Balanced Fargate Service
    const applicationLoadBalancerFargateService: ApplicationLoadBalancedFargateService = new ApplicationLoadBalancedFargateService(
        this,
        `${_props.containerConfig.name}-service`,
        {
            serviceName: `${_props.containerConfig.name}-service`,
            cluster: cluster, // Required
            cpu: 256, // Default is 256
            desiredCount: 1, // Default is 1
            taskDefinition: taskDefinition,
            memoryLimitMiB: 512, // Default is 512
            publicLoadBalancer: true, // Default is false
            loadBalancerName: `${_props.containerConfig.name}-ALB`,
        },
    );

    return {
        cluster: cluster,
        service: applicationLoadBalancerFargateService
    };
}

Enter fullscreen mode Exit fullscreen mode

Explanation of the Code:
Method Signature:

private createEcs(_props: ICreateEcs, _account: string, _region: string, _ecrName: string) { ... }
This method takes in the configuration details for ECS (_props), AWS account ID (_account), AWS region (_region), and the ECR repository name (_ecrName) as arguments.
Execution Role for ECS Task:

We create a new Role named executionRole for the ECS task.
assumedBy: Specifies that this role can be assumed by the ECS Fargate service.
addToPolicy: Adds permissions to the role for tasks to interact with ECR and CloudWatch Logs.
Fargate Task Definition:

We define a FargateTaskDefinition that specifies the properties of our ECS task.
executionRole: Specifies the execution role for the task.
runtimePlatform: Specifies the CPU architecture and operating system family.
Adding Container to Task Definition:

We add a container to the task definition.
image: Specifies the Docker image for our Nest.js application, retrieved from the ECR repository.
containerName: Sets the name of the container.
portMappings: Maps the container port to the host port for communication.
Creating VPC for ECS Cluster:

We create a new Amazon VPC (Virtual Private Cloud) for our ECS cluster.
This VPC provides an isolated environment for our ECS resources.
Creating ECS Cluster:

We create an ECS cluster within the VPC.
clusterName: Specifies the name of the ECS cluster.
vpc: Specifies the VPC in which the cluster will be created.
Creating Application Load Balanced Fargate Service:

We create an Application Load Balanced Fargate service.
serviceName: Specifies the name of the ECS service.
cluster: Specifies the ECS cluster to which the service will be deployed.
cpu: Specifies the CPU units for the Fargate tasks.
desiredCount: Specifies the number of Fargate tasks to run.
taskDefinition: Specifies the task definition to use for the service.
memoryLimitMiB: Specifies the memory limit for the Fargate tasks.
publicLoadBalancer: Specifies whether the load balancer should be public or private.
loadBalancerName: Specifies the name of the load balancer for the service.
Return:

Finally, we return an object containing the created cluster and the service for further usage in our application.
By executing this code, we create an AWS ECS cluster and an ECS Fargate service for our Nest.js application. The Fargate service will run our containerized application, ensuring scalability and high availability. Additionally, an Application Load Balancer (ALB) is set up to distribute incoming traffic across our Fargate tasks.

With the ECS cluster and Fargate service in place, we now have a robust and scalable infrastructure to deploy our Nest.js application. Our continuous integration pipeline, combined with ECS Fargate, offers a seamless deployment process that automates building, testing, and deploying our application updates.

We are now ready to witness the full power of AWS CodePipeline and ECS Fargate as we deploy our Nest.js application effortlessly. Let's proceed to the final steps of our deployment journey!

Fourth Step : Automating Service Updates with AWS CodePipeline :

In the final step of our deployment process, we will automate the updating of our ECS Fargate service whenever a new version of our Nest.js application is built and ready for deployment. This automation will ensure seamless updates to our running service without manual intervention.

In this step, we define the attachDeployAction method within our EcsApplicationConstruct class. This method is responsible for creating a CodeBuild project to update the ECS Fargate service with the latest version of our container image.

attachDeployAction(pipeline: Pipeline, buildOutput: Artifact, cluster: Cluster, service: ApplicationLoadBalancedFargateService) {
    // Create CodeBuild Project for Updating Service
    const updateTaskDefinition = new PipelineProject(this, `UpdateServiceProject`, {
        buildSpec: BuildSpec.fromObject({
            version: '0.2',
            phases: {
                build: {
                    commands: [
                        `aws ecs update-service --cluster ${cluster.clusterName} --service ${service.service.serviceArn} --force-new-deployment`
                    ],
                },
            },
        }),
    });

    // Add Permissions to Update Task Definition
    updateTaskDefinition.addToRolePolicy(new PolicyStatement({
        resources: ["*"],
        actions: ['ecs:*'],
        effect: Effect.ALLOW
    }));

    // Add Stage to CodePipeline for Updating Service
    pipeline.addStage({
        stageName: "UpdateService",
        actions: [new CodeBuildAction({
            actionName: 'UpdateService',
            project: updateTaskDefinition,
            input: buildOutput,
        })]
    });
}

Enter fullscreen mode Exit fullscreen mode

Explanation of the Code:
Method Signature:

attachDeployAction(pipeline: Pipeline, buildOutput: Artifact, cluster: Cluster, service: ApplicationLoadBalancedFargateService) { ... }
This method takes in the CodePipeline pipeline, buildOutput artifact, ECS cluster, and service as arguments.
Create CodeBuild Project for Updating Service:

We create a new PipelineProject named updateTaskDefinition for updating the ECS Fargate service.
buildSpec: Specifies the build specifications as a set of commands.
In this case, we run the command to update the ECS service with the new container image.
--force-new-deployment: Ensures that the ECS service is updated with the latest changes.
Add Permissions to Update Task Definition:

We add permissions to the updateTaskDefinition to allow it to interact with ECS.
This policy grants necessary permissions for updating the ECS service.
Add Stage to CodePipeline for Updating Service:

We add a new stage named "UpdateService" to the CodePipeline.
This stage will trigger the updateTaskDefinition to update the ECS Fargate service.
The buildOutput artifact from the previous stage is used as input for this stage.
Return:

There is no explicit return in this method, as the changes are directly applied to the provided pipeline.
Deployment Automation in Action
With this setup, whenever a new version of our Nest.js application is built and stored in the Amazon ECR repository, the CodePipeline will automatically trigger the "UpdateService" stage. This stage will execute the updateTaskDefinition, which in turn updates the ECS Fargate service with the latest container image.

This automation ensures that our ECS Fargate service is always running the latest version of our Nest.js application without manual intervention. Any updates to our codebase will flow seamlessly from the development environment to the production environment.

Conclusion
Congratulations! We have successfully set up a complete deployment pipeline for our Nest.js application using AWS CodePipeline and ECS Fargate. From automatically building our application, storing it in Amazon ECR, deploying it to an ECS Fargate service, to updating the running service with new versions, we have covered the entire deployment lifecycle.

By leveraging these powerful AWS services and infrastructure as code with AWS CDK, we have established an efficient, scalable, and automated deployment process for our Nest.js application. This not only saves time but also ensures consistency and reliability in our deployments.

With our deployment pipeline in place, we can focus more on developing and improving our Nest.js application, knowing that the deployment process is taken care of. We hope this article has been informative and helpful in your journey to deploying applications on AWS. Happy coding!

Top comments (0)