(aws_ecs_patterns): QueueProcessingFargateService should be able to provide Backlog per instance based autoscaling for ECS tasks
- Dominant language
- TypeScript
- Stars
- 12.9k
- Forks
- 4.6k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 83
Description
### Describe the feature
While working with the QueueProcessingFargateService, I noticed that there are currently two supported scaling strategies for providing autoscaling for the Fargate Service that can be configured, based on the following metrics:
- CPU Based Scaling
- ApproximateNumberOfMessagesVisible
However, there is a scaling strategy that is recommended in the AWS documentation itself, which seems very useful in cases with where we want to have a highly performant asynchronous system based on acceptable latency i.e. the Backlog per instance based autoscaling strategy [Autoscaling using custom SQS metric](https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-using-sqs-queue.html#scale-sqs-queue-custom-metric)
Currently there is no native way in the QueueProcessingFargateService to plug a custom scaling strategy, which uses custom metrics. Especially, to achieve the same scaling strategy as mentioned in the documentation, it is not possible to use Metric Math expression (L2 construct) because some of the required properties are only present in the Metric object and not the IMetric interface which is implemented by MathExpression.
This is the line of code that checks for the additional properties
[`aws-cdk/blob/main/packages/aws-cdk-lib/aws-autoscaling/lib/target-tracking-scaling-policy.ts`](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/aws-autoscaling/lib/target-tracking-scaling-policy.ts#L123)
For example,
```
class QueueProcessingFargateServiceWithBacklogPerInstanceScaling extends QueueProcessingFargateService {
constructor(scope: Construct, id: string, props: QueueProcessingFargateServiceProps) {
super(scope, id, props);
}
protected override configureAutoscalingForService(service: BaseService) {
const scaling = service.autoScaleTaskCount({
minCapacity: 1,
maxCapacity: 10
});
scaling.scaleToTrackCustomMetric('CustomSQSScaling', {
metric: new cloudwatch.MathExpression({
expression: "m1 / m2",
usingMetrics: {
m1: queue.metricApproximateNumberOfMessagesVisible(),
m2: service.metric("RunningTaskCount"),
},
}),
targetValue: 10
});
}
}
```
Using L2 constructs to achieve the recommended scaling will result into the error
> Only direct metrics are supported for Target Tracking. Use Step Scaling or supply a Metric object.
This is currently also an open issue #20659 that mentions the same error.
To achieve a workaround, it is possible to use L1 CustomScalingPolicy instead
```
class QueueProcessingFargateServiceWithBacklogPerInstanceScaling extends QueueProcessingFargateService {
constructor(scope: Construct, id: string, props: QueueProcessingFargateServiceProps) {
super(scope, id, props);
}
protected override configureAutoscalingForService(service: BaseService) {
const serviceScalableTarget = new aws_applicationautoscaling.ScalableTarget(this,
"serviceScalableTarget",
{
serviceNamespace: aws_applicationautoscaling.ServiceNamespace.ECS,
scalableDimension: "ecs:service:DesiredCount",
resourceId: `service/${this.cluster.clusterName}/${service.serviceName}`,
minCapacity: this.minCapacity,
maxCapacity: this.maxCapacity,
}
)
const backlogPerInstanceCustomScalingMetric = {
metrics: [
{
id: "sqsBacklogPerECSTask",
label: "SQSBacklogPerECSTask",
expression: "approximateNumberOfMessagesVisible / desiredTaskCount",
returnData: true,
},
{
id: "desiredTaskCount",
label: "DesiredTaskCount",
metricStat: {
metric: {
namespace: "ECS/ContainerInsights",
metricName: "DesiredTaskCount",
dimensions: [
{ name: "ClusterName", value: service.cluster.clusterName },
{ name: "ServiceName", value: service.serviceName },
],
},
stat: "Average",
},
returnData: false,
},
{
id: "approximateNumberOfMessagesVisible",
label: "ApproximateNumberOfMessagesVisible",
metricStat: {
metric: {
namespace: "AWS/SQS",
metricName: "ApproximateNumberOfMessagesVisible",
dimensions: [
{ name: "QueueName", value: this.sqsQueue.queueName },
],
},
stat: "Average",
},
returnData: false,
},
],
}
new CfnScalingPolicy(this, 'BacklogTargetTracking', {
policyName: 'BacklogPerTaskTargetTracking',
policyType: 'TargetTrackingScaling',
scalingTargetId: serviceScalableTarget.scalableTargetId,
targetTrackingScalingPolicyConfiguration: {
targetValue: 100,
customizedMetricSpecification: backlogPerInstanceCustomScalingMetric,
scaleInCooldown: 300,
scaleOutCooldown: 120,
},
});
}
}
```
However, as you can notice, this requires us to override the parent method and this is not the best way to do it, Hence, I think there can be a more modular, built-in way of achieving the same result.
### Use Case
1. Efficient Auto Scaling: Enable ECS Fargate service to scale more accurately based on real-time workload pressure, not just raw queue size.
2. Cost Optimization: Prevent over-provisioning by scaling only when messages per instance exceed a meaningful threshold.
3. Latency Control: Maintain consistent message processing latency by ensuring tasks are added only when processing backlog grows per instance
### Proposed Solution
_No response_
### Other Information
_No response_
### Acknowledgements
- [x] I may be able to implement this feature request
- [ ] This feature might incur a breaking change
### CDK version used
2.1005.0
### Environment details (OS name and version, etc.)
OS: Mac OS, Language: TypeScript, Node Version: 22.14.0
Contributor guide
Research direction
Start with QueueProcessingFargateService and its configureAutoscalingForService entry point, then read aws-autoscaling/lib/target-tracking-scaling-policy.ts around the direct-metric check. Compare the built-in CPU and ApproximateNumberOfMessagesVisible strategies with the documented custom-metric workaround. Done means the service offers a modular way to configure backlog-per-instance target tracking without overriding the parent method.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, typescript
- Domain
- cloud, devops
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100