Posts Simple API infrastructure as code
Post
Cancel

Simple API infrastructure as code

Introduction

In my previous post I compared the infrastructure code needed to solve a simple event-driven use case using a serverless backend versus a containerized one. In this post I compare the infrastructure code needed to handle an auto scaling API endpoint using a serverless backend (Lambda) and a containerized one (ECS + Fargate).

As before, I used the AWS CDK to define the infrastructure. I also made an AWS SAM version that I won’t detail here but you can find in the Git repository.

API Gateway vs. Application Load Balancer

One difference of note was my choice of api gateways. The Lambda architecture includes an HTTP API Gateway while the ECS version uses an Application Load Balancer (ALB). I could have used an ALB with the Lambda function or an HTTP API Gateway in front of the ECS cluster. (If you are curious about the differences, here’s a good comparison.)

Lacking any requirements that would force me to choose one over the other (e.g., the ability to throttle requests or handle a really high request rate), I selected the option that created the simplest infrastructure. For Lambda, this was API Gateway. For ECS, it was easier to use just an ALB because, had I used an API Gateway, I still would have needed a load balancer behind it for auto scaling.

Lambda

The CDK code to define an API route backed by a Lambda function is pretty simple in large part because no VPC is required and AWS handles Lambda scaling.

1
2
3
4
5
6
7
8
9
10
11
12
const handler = new lambda.Function(this, 'handler', {
  code: lambda.Code.fromAsset('src/'),
  handler: 'index.handler',
  runtime: lambda.Runtime.NODEJS_12_X
})

const api = new HttpApi(this, 'iac-api-lambda')
api.addRoutes({
  methods: [ HttpMethod.GET ],
  path: '/',
  integration: new LambdaProxyIntegration({ handler })
  })

API Gateway integrations connect API routes to the backend services that implement their business logic. In this case the backend is a Lambda function so I used a Lambda proxy integration to hook up the GET / route to my code.

Side note: Integrations are a great way to simplify and save money on your infrastructure because of the other AWS services they work with. For example, instead of maintaining the infrastructure and code that sends a message to an SQS queue, puts a record in a Kinesis stream or initiates a Step Function, API Gateway can forward API requests directly to those services.

ECS + Fargate

The CDK code defining an Application Load Balancer with an autoscaling Fargate service is really straightforward considering the complexity of the infrastructure it creates:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const cluster = new ecs.Cluster(this, 'cluster', {
  vpc: new ec2.Vpc(this, 'vpc', {
    maxAzs: 2
  })
})

const service = new ApplicationLoadBalancedFargateService(this, 'service', {
  cluster,
  taskImageOptions: {
    image: ecs.ContainerImage.fromAsset('src/')
  },
  publicLoadBalancer: true
})

const scaling = service.service.autoScaleTaskCount({
  maxCapacity: 4
})
scaling.scaleOnCpuUtilization('scale-metric', {
  targetUtilizationPercent: 75
})

It’s this simple largely because of the CDK’s ApplicationLoadBalancedFargateService construct. When you create one of these, the CDK:

  • Creates an Application Load Balancer
  • Connects the load balancer to the VPC’s public subnets
  • Defines a security group allowing HTTP traffic to the load balancer
  • Creates an ECS task definition and service
  • Directs traffic from the load balancer to the ECS service

The last step was to define the rules by which the load balanced service should scale. The code above defines a simple CPU utilization-based scaling policy. Of course you will need to load test your application to find the most suitable scaling metric. For example, your service might run out of network bandwidth before overloading the CPU.

Conclusion

The conclusions around control and costs from my previous post still apply to this use case. However, a synchronous public API, as opposed to the asynchronous backend service previously covered, creates additional infrastructure complexity around load balancing and auto scaling when using a containerized architecture.

Resources

From the code above, both architectures look pretty simple and straightforward. However, operationally they are quite different.

The Lambda infrastructure closely matches the CDK code:

API Gateway to Lambda architecture

The only resources created but not shown above are the Lambda’s IAM Role and the permission API Gateway needs to invoke the function.

The ECS + Fargate code creates a much more complex infrastructure:

Load balanced ECS/Fargate architecture

Despite all that complexity, the diagram above doesn’t show IAM roles, security groups, scaling policies, etc. All in all, the deployed Lambda CloudFormation stack creates 8 resources and the ECS stack creates 40!

Finally

As this post and the previous one try to demonstrate, containerized architectures tend to cost more and have more pieces to manage compared to serverless architectures. What benefits do you get from this added cost complexity?

Really, I’m asking. Let me know what you think in the comments below.

This post is licensed under CC BY 4.0 by the author.