Guide to using AWS ECS Fargate

Introduction

AWS Elastic Container Service (ECS) Fargate is a "serverless" technology for containers, helping you run Docker applications without having to manage physical servers or EC2 virtual machine clusters.

Normally, when running containers, you must choose the instance type, manage the operating system, and scale the server cluster. With Fargate, you simply package the application into a container, define CPU and memory requirements, and Fargate will automatically initialize and manage the underlying infrastructure.

Key Advantages

  • No infrastructure management: Eliminates operational burdens such as OS patching, server maintenance, or EC2 cluster management.
  • High security: Each task runs in a separate execution environment, completely isolated from other tasks, helping to enhance security.
  • Flexible scalability: Fargate automatically adjusts resources based on the actual needs of the application without worrying about cluster capacity shortages.
  • Deep integration: Easily connect with other AWS services such as IAM, CloudWatch, and VPC.

Pricing

Fargate applies a Pay-as-you-go model (pay for what you use). Costs are calculated based on:

  • The amount of CPU and Memory (RAM) that your container configures.
  • Running time: Calculated from the start of downloading the container image until the task terminates, accurate to the second.
  • Additional costs: May include data storage fees (EBS), data transfer fees (Data Transfer), or fees for using other advanced features.

Prerequisites

In this article, I will still use the Docker Image built from the NestJS project; you can follow my previous articles to continue using it or use your own docker image, then push this docker image to AWS ECR.

Detail

In the AWS CDK project, create a .env file with the following information; please update it accordingly to suit your needs.

CDK_DEFAULT_REGION = ap-southeast-1
BUCKET = <BUCKET>
IMAGE = <IMAGE>
CUSTOM_HEADER = <CUSTOM_HEADER>
VERIFY_SECRET = <VERIFY_SECRET>


Create file lib/ecs-fargate-cloudfront-stack.ts

import * as cdk from "aws-cdk-lib"
import * as cloudfront from "aws-cdk-lib/aws-cloudfront"
import * as origins from "aws-cdk-lib/aws-cloudfront-origins"
import * as ec2 from "aws-cdk-lib/aws-ec2"
import * as ecs from "aws-cdk-lib/aws-ecs"
import * as ecs_patterns from "aws-cdk-lib/aws-ecs-patterns"
import * as iam from "aws-cdk-lib/aws-iam"
import * as s3 from "aws-cdk-lib/aws-s3"
import { Construct } from "constructs"

export class EcsFargateCloudfrontStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const vpc = new ec2.Vpc(this, "NestVpc", {
      maxAzs: 2,
      natGateways: 0,
      subnetConfiguration: [
        { name: "Public", subnetType: ec2.SubnetType.PUBLIC },
      ],
    })

    const imageUri = process.env.IMAGE || ""
    const bucket = process.env.BUCKET || ""
    const customHeader = process.env.CUSTOM_HEADER || ""
    const originVerifySecret = process.env.VERIFY_SECRET || ""

    const fargateService =
      new ecs_patterns.ApplicationLoadBalancedFargateService(
        this,
        "NestService",
        {
          vpc,
          cpu: 256,
          memoryLimitMiB: 512,
          assignPublicIp: true,
          circuitBreaker: { rollback: true },
          capacityProviderStrategies: [
            {
              capacityProvider: "FARGATE_SPOT",
              weight: 1,
            },
          ],
          taskImageOptions: {
            image: ecs.ContainerImage.fromRegistry(imageUri),
            containerPort: 3000,
            environment: {
              REGION: process.env.CDK_DEFAULT_REGION || "",
              BUCKET: bucket,
              VERIFY_SECRET: originVerifySecret,
            },
          },
          healthCheckGracePeriod: cdk.Duration.seconds(120),
        },
      )

    fargateService.taskDefinition.addToExecutionRolePolicy(
      new iam.PolicyStatement({
        actions: [
          "ecr:GetAuthorizationToken",
          "ecr:BatchCheckLayerAvailability",
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage",
        ],
        resources: ["*"],
      }),
    )

    fargateService.targetGroup.configureHealthCheck({
      path: "/health",
      port: "3000",
      healthyThresholdCount: 2,
      unhealthyThresholdCount: 5,
      interval: cdk.Duration.seconds(60),
      timeout: cdk.Duration.seconds(5),
    })

    const myBucket = s3.Bucket.fromBucketName(this, "ExistingBucket", bucket)
    fargateService.taskDefinition.addToTaskRolePolicy(
      new iam.PolicyStatement({
        actions: ["s3:PutObject", "s3:GetObject", "s3:ListBucket"],
        resources: [
          myBucket.bucketArn,
          myBucket.arnForObjects("*"),
        ],
      }),
    )

    const distribution = new cloudfront.Distribution(this, "NestDist", {
      defaultBehavior: {
        origin: new origins.LoadBalancerV2Origin(fargateService.loadBalancer, {
          customHeaders: {
            [customHeader]: originVerifySecret,
          },
          protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
        }),
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
      },
    })

    new cdk.CfnOutput(this, "URL", {
      value: `https://${distribution.distributionDomainName}`,
    })
  }
}

Explaination:

  • vpc: still creates a VPC (Virtual Private Cloud) without a NAT Gateway (as this resource is quite expensive)
  • fargateService: created along with a Load Balancer; you pass in the cpu, memoryLimitMiB, and the vpc created above
  • capacityProvider: using FARGATE_SPOT can save up to 70% in costs for applications that can tolerate interruptions; since this is just an example, I use it to save costs, but in actual cases, you should consider this option
  • taskImageOptions: pass in docker image information such as image url, port, and environment variables
  • fargateService.taskDefinition: To grant ECR login permissions to the Execution Role
  • fargateService.targetGroup.configureHealthCheck: in case you have an API for health checks, add it; otherwise, it can be skipped
  • fargateService.taskDefinition.addToTaskRolePolicy: this part is to grant permissions to the S3 Bucket
  • cloudfront.Distribution: creates Cloudfront pointing to the Load Balancer address, along with customHeaders; note that the NestJS project must also have middleware to handle customHeaders (I have provided instructions in previous articles)


After deploying, the results are as follows:

 EcsFargateCloudfrontStack
 Deployment time: 119.34s

Outputs:
EcsFargateCloudfrontStack.NestServiceLoadBalancerDNSDB906E33 = EcsFar-NestS-IPmtJv46ZBvk-1966896999.ap-southeast-1.elb.amazonaws.com
EcsFargateCloudfrontStack.NestServiceServiceURLA979D4F3 = http://EcsFar-NestS-IPmtJv46ZBvk-1966896999.ap-southeast-1.elb.amazonaws.com
EcsFargateCloudfrontStack.URL = https://d2lb28x1e93vqe.cloudfront.net

 Total time: 132.06s


The corresponding resources have been created on the AWS Console



Use Postman to use the API


Happy coding!

See more articles here.

Comments

Popular posts from this blog

All practice series

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Using Kafka with Docker and NodeJS

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Kubernetes Practice Series

Kubernetes Deployment for Zero Downtime

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

NodeJS Practice Series

Helm for beginer - Deploy nginx to Google Kubernetes Engine