Senior Software Engineer
Published on

Building a Serverless Microservice with AWS CDK and ECS

cover

Introduction

With the increasing popularity of microservice architectures, developers are increasingly turning to serverless technologies like AWS Lambda and API Gateway to build scalable and resilient services. However, building and deploying a serverless microservice can be a complex task, requiring knowledge of AWS infrastructure and configuration. AWS Cloud Development Kit (CDK) is a powerful tool that allows developers to define infrastructure as code using familiar programming languages like TypeScript. In this tutorial, we'll show you how to use AWS CDK to build a serverless microservice that includes an ECS cluster with auto-scaling, an Application Load Balancer, and API Gateway with private integration and AWS Cognito user pool authorizer. By the end of this tutorial, you'll have a fully functional microservice that can be easily deployed and managed using AWS CDK.

Setup AWS CDK

In order to create new project with CDK :

  1. Install AWS CLI and AWS CDK by following the instructions on the official AWS CDK website.
  2. Create a new directory for your project and navigate to it.
  3. Initialize a new AWS CDK project by running the following command:
cdk init --language typescript
  1. This will create a new directory with a sample AWS CDK project in TypeScript.
  2. Open the project in your favorite code editor.
  3. Install the AWS SDK and any other dependencies you need by running npm install in your project directory.
  4. Start building your AWS CDK stack by editing the lib/<StackName>.ts file.

That's it! You should now be able to start building your AWS CDK project in TypeScript.

Setting up the VPC and ECS Cluster

To begin, we'll set up a VPC with private and public subnets, and an ECS cluster to host our microservice. We'll also configure auto-scaling based on CPU utilization to ensure that our microservice can handle spikes in traffic.

First, we'll create a new VPC using the ec2.Vpc construct from the AWS CDK library. We'll specify a CIDR block for the VPC, as well as the number of NAT gateways and availability zones. We'll also configure the VPC to have two subnets - one private subnet with no direct internet access, and one public subnet with internet access.

const vpc = new ec2.Vpc(this, 'MyVpc', {
  cidr: '10.0.0.0/16',
  maxAzs: 2,
  natGateways: 1,
  subnetConfiguration: [
    {
      name: 'private-subnet-1',
      subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
      cidrMask: 24,
    },
    {
      name: 'public-subnet-1',
      subnetType: ec2.SubnetType.PUBLIC,
      cidrMask: 24,
    }
  ],
});

Next, we'll create a new ECS cluster using the ecs.Cluster construct. We'll specify the VPC we just created as the cluster's network, and also give it a name.

const cluster = new ecs.Cluster(this, 'MyCluster', {
  vpc: vpc,
  clusterName: "ecs-microservice",
});

Finally, we'll add auto-scaling to our ECS cluster based on CPU utilization. We'll create an auto-scaling group using the cluster.addCapacity method, and specify an instance type, minimum and maximum number of instances, and a scaling policy based on CPU utilization. This will ensure that our microservice can handle spikes in traffic without becoming overwhelmed.

const autoScalingGroup = cluster.addCapacity('capacity', {
  instanceType: new ec2.InstanceType('t3.large'),
  minCapacity: 1,
  maxCapacity: 3,
});

const cpuMetric = cluster.metricCpuUtilization();
const cpuScaling = autoScalingGroup.scaleOnMetric('CpuScaling', {
  metric: cpuMetric,
  adjustmentType: applicationautoscaling.AdjustmentType.CHANGE_IN_CAPACITY,
  scalingSteps: [
    { change: -1, lower: 0, upper: 23 },
    { change: +1, lower: 23, upper: 75 },
    { change: +1, lower: 75 },
  ],
});

With these steps, we have created a VPC with private and public subnets, and an ECS cluster with auto-scaling based on CPU utilization. In the next step, we'll create a task definition and containers for our microservice.

Creating task definition and LB (Load Balancer)

import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';

// Set up the task definition for the microservice
const taskDefinition = new ecs.TaskDefinition(this, 'MyTaskDefinition', {
  compatibility: ecs.Compatibility.EC2,
  cpu: '512',
  memoryMiB: '2048',
});

// Add a container to the task definition
const container = taskDefinition.addContainer('MyContainer', {
  image: ecs.ContainerImage.fromRegistry('my-registry/my-image:latest'),
  memoryLimitMiB: 512,
  cpu: 256,
});
container.addPortMappings({
  containerPort: 80,
  protocol: ecs.Protocol.TCP,
});

// Set up the load balancer
const lb = new elbv2.ApplicationLoadBalancer(this, 'MyLoadBalancer', {
  vpc: vpc,
  internetFacing: true,
});
const listener = lb.addListener('MyListener', {
  port: 80,
});
const targetGroup = listener.addTargets('MyTargetGroup', {
  port: 80,
  targets: [new ecs.EcsTarget(service)],
});

In this example, we first import the necessary AWS CDK libraries for ECS and Elastic Load Balancing v2. We then create a new task definition using the ecs.TaskDefinition class, specifying a compatibility mode (in this case, EC2), CPU and memory limits, and adding a container to the task definition. We set the container image to be pulled from a registry and map port 80 for the container.

Next, we set up the load balancer using the elbv2.ApplicationLoadBalancer class, passing in the VPC we created earlier and specifying that it should be internet-facing. We then create a listener on port 80 for the load balancer and add a target group for the listener. We pass in the ECS service we created earlier as a target for the target group.

This will allow us to route incoming traffic to our microservice via the load balancer.

Setting up API Gateway with Private Integration

API Gateway is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale. It can integrate with a range of AWS services and supports both public and private APIs. Private APIs can be accessed from within a VPC, allowing access to resources that are not publicly accessible.

To set up API Gateway with private integration, you'll need to:

  1. Create a VPC Link: A VPC Link enables private communication between your VPC and API Gateway. You can create a VPC Link using the AWS CDK by creating a apigatewayv2.VpcLink object and specifying the VPC and a name for the link.

  2. Create a Target Group: A Target Group is a group of resources that you can route traffic to using a load balancer. In this case, you'll create a Target Group for your ECS service. You can create a Target Group using the AWS CDK by creating a lb.ApplicationTargetGroup object and specifying the port and protocol.

  3. Create an HTTP Integration: An HTTP Integration is a way to integrate an HTTP endpoint with your API Gateway. In this case, you'll create an HTTP Integration that points to your Target Group. You can create an HTTP Integration using the AWS CDK by creating a apigatewayv2_integrations.HttpAlbIntegration object and specifying the name of the integration, the listener to integrate with, and the VPC Link to use.

  4. Configure the API Gateway Route: Finally, you'll configure the API Gateway route to use the HTTP Integration you just created. You can do this using the AWS CDK by adding a route to your apigatewayv2.HttpApi object and specifying the path, method, and integration to use.

// Create a VPC Link
const link = new apigatewayv2.VpcLink(this, 'MyVpcLink', {
  vpc,
  vpcLinkName: 'MyVpcLink'
});

// Create a Target Group
const targetGroup = new lb.ApplicationTargetGroup(this, 'MyTargetGroup', {
  vpc,
  port: 80,
  protocol: lb.ApplicationProtocol.HTTP,
  targets: [service.loadBalancerTarget({
    containerName: container.containerName,
    containerPort: container.containerPort,
    protocol: ecs.Protocol.TCP
  })],
  healthCheck: {
    interval: cdk.Duration.seconds(60),
    path: "/health",
    timeout: cdk.Duration.seconds(5),
  },
});

// Create an HTTP Integration
const integration = new apigatewayv2_integrations.HttpAlbIntegration('MyIntegration', listener, {
  vpcLink: link,
});

// Configure the API Gateway Route
const httpApi = new apigatewayv2.HttpApi(this, 'MyHttpApi');
httpApi.addRoutes({
  path: '/my-route',
  methods: [apigatewayv2.HttpMethod.GET],
  integration: integration,
});

In this example, the VPC Link is created using the apigatewayv2.VpcLink object, the Target Group is created using the lb.ApplicationTargetGroup object, and the HTTP Integration is created using the apigatewayv2_integrations.HttpAlbIntegration object. Finally, the API Gateway route is added using the apigatewayv2.HttpApi object and the addRoutes method.

Creating a Route53 A record for the API

this step involves creating a Route53 A record for the API. This allows clients to access the API using a custom domain name instead of the default API Gateway domain name. To create a Route53 A record, we first need to create a Hosted Zone in Route53 for our domain name. This is typically done in the AWS Management Console, but it can also be done using the AWS CDK.

Assuming we have already created a Hosted Zone, we can create a new A record using the following code:

import * as route53 from "aws-cdk-lib/aws-route53";
import * as route53Targets from "aws-cdk-lib/aws-route53-targets";
import * as apigatewayv2 from '@aws-cdk/aws-apigatewayv2-alpha';

// ...

const apiDomain = `ecs.${domainName}`;

// Lookup hosted zone from details above 
const hostedZone = route53.HostedZone.fromHostedZoneAttributes(this, `ecs-hostedzone`, {
  hostedZoneId: HostedZoneid,
  zoneName: HostedZonename
});

// Create a new A record for the API
new route53.ARecord(this, `ecs-dns-record`, {
  zone: hostedZone,
  recordName: apiDomain,
  target: route53.RecordTarget.fromAlias(
    new route53Targets.ApiGatewayv2DomainProperties(dn.regionalDomainName, dn.regionalHostedZoneId)
  ),
});

This creates a new A record in Route53 for the custom domain name ecs.domainName and points it to the API Gateway using an alias. The ApiGatewayv2DomainProperties object is used to configure the alias with the regional domain name and hosted zone ID for the API Gateway domain.

Conclusion

In this post, we've explored how to build and deploy a microservice architecture on AWS using the CDK with TypeScript. We started by setting up a VPC and ECS cluster, then created a microservice and load balancer to serve traffic. We added API Gateway with a private integration to provide secure access to the microservice, and created a Route53 A record for the API to enable custom domain name access.

Using the AWS CDK, we were able to automate the creation and deployment of our infrastructure with minimal effort. This allowed us to focus on developing our microservice instead of worrying about manual infrastructure setup.

Next Steps

There are many other features and services we could add to this architecture to make it even more robust and secure. Some ideas include:

Adding AWS Fargate or AWS Lambda as a compute option for the microservice. Adding a CDN to cache static assets and improve performance. Adding more advanced security features like AWS WAF or AWS Shield. Adding more complex routing rules and integrations with API Gateway. The AWS CDK makes it easy to add and modify infrastructure components as our needs evolve, so we can continue to build and improve our architecture over time.