Configure AWS Application Auto Scaling Targets

The aws:appautoscaling/target:Target resource, part of the Pulumi AWS provider, registers AWS resources as scalable targets, defining their capacity bounds and enabling Application AutoScaling to adjust their size. This guide focuses on four capabilities: DynamoDB table and index capacity, ECS service task counts, Aurora read replicas, and MSK broker storage.

Scalable targets reference existing AWS resources and typically rely on AWS-managed IAM Service-Linked Roles. The examples are intentionally small. Combine them with your own scaling policies (via aws.appautoscaling.Policy) and monitoring.

Scale DynamoDB table read capacity

Applications using DynamoDB often need to scale read capacity automatically as traffic patterns change throughout the day.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const dynamodbTableReadTarget = new aws.appautoscaling.Target("dynamodb_table_read_target", {
    maxCapacity: 100,
    minCapacity: 5,
    resourceId: `table/${example.name}`,
    scalableDimension: "dynamodb:table:ReadCapacityUnits",
    serviceNamespace: "dynamodb",
});
import pulumi
import pulumi_aws as aws

dynamodb_table_read_target = aws.appautoscaling.Target("dynamodb_table_read_target",
    max_capacity=100,
    min_capacity=5,
    resource_id=f"table/{example['name']}",
    scalable_dimension="dynamodb:table:ReadCapacityUnits",
    service_namespace="dynamodb")
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/appautoscaling"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := appautoscaling.NewTarget(ctx, "dynamodb_table_read_target", &appautoscaling.TargetArgs{
			MaxCapacity:       pulumi.Int(100),
			MinCapacity:       pulumi.Int(5),
			ResourceId:        pulumi.Sprintf("table/%v", example.Name),
			ScalableDimension: pulumi.String("dynamodb:table:ReadCapacityUnits"),
			ServiceNamespace:  pulumi.String("dynamodb"),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var dynamodbTableReadTarget = new Aws.AppAutoScaling.Target("dynamodb_table_read_target", new()
    {
        MaxCapacity = 100,
        MinCapacity = 5,
        ResourceId = $"table/{example.Name}",
        ScalableDimension = "dynamodb:table:ReadCapacityUnits",
        ServiceNamespace = "dynamodb",
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.appautoscaling.Target;
import com.pulumi.aws.appautoscaling.TargetArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var dynamodbTableReadTarget = new Target("dynamodbTableReadTarget", TargetArgs.builder()
            .maxCapacity(100)
            .minCapacity(5)
            .resourceId(String.format("table/%s", example.name()))
            .scalableDimension("dynamodb:table:ReadCapacityUnits")
            .serviceNamespace("dynamodb")
            .build());

    }
}
resources:
  dynamodbTableReadTarget:
    type: aws:appautoscaling:Target
    name: dynamodb_table_read_target
    properties:
      maxCapacity: 100
      minCapacity: 5
      resourceId: table/${example.name}
      scalableDimension: dynamodb:table:ReadCapacityUnits
      serviceNamespace: dynamodb

The resourceId property identifies the table using the format table/{table-name}. The scalableDimension specifies which metric to scale (here, read capacity units). The serviceNamespace tells Application AutoScaling which AWS service manages this resource. Together, minCapacity and maxCapacity define the scaling bounds: Application AutoScaling adjusts provisioned throughput between 5 and 100 units based on demand.

Scale DynamoDB global secondary index capacity

Global secondary indexes often have different access patterns than their base tables, requiring independent scaling.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const dynamodbIndexReadTarget = new aws.appautoscaling.Target("dynamodb_index_read_target", {
    maxCapacity: 100,
    minCapacity: 5,
    resourceId: `table/${example.name}/index/${indexName}`,
    scalableDimension: "dynamodb:index:ReadCapacityUnits",
    serviceNamespace: "dynamodb",
});
import pulumi
import pulumi_aws as aws

dynamodb_index_read_target = aws.appautoscaling.Target("dynamodb_index_read_target",
    max_capacity=100,
    min_capacity=5,
    resource_id=f"table/{example['name']}/index/{index_name}",
    scalable_dimension="dynamodb:index:ReadCapacityUnits",
    service_namespace="dynamodb")
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/appautoscaling"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := appautoscaling.NewTarget(ctx, "dynamodb_index_read_target", &appautoscaling.TargetArgs{
			MaxCapacity:       pulumi.Int(100),
			MinCapacity:       pulumi.Int(5),
			ResourceId:        pulumi.Sprintf("table/%v/index/%v", example.Name, indexName),
			ScalableDimension: pulumi.String("dynamodb:index:ReadCapacityUnits"),
			ServiceNamespace:  pulumi.String("dynamodb"),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var dynamodbIndexReadTarget = new Aws.AppAutoScaling.Target("dynamodb_index_read_target", new()
    {
        MaxCapacity = 100,
        MinCapacity = 5,
        ResourceId = $"table/{example.Name}/index/{indexName}",
        ScalableDimension = "dynamodb:index:ReadCapacityUnits",
        ServiceNamespace = "dynamodb",
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.appautoscaling.Target;
import com.pulumi.aws.appautoscaling.TargetArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var dynamodbIndexReadTarget = new Target("dynamodbIndexReadTarget", TargetArgs.builder()
            .maxCapacity(100)
            .minCapacity(5)
            .resourceId(String.format("table/%s/index/%s", example.name(),indexName))
            .scalableDimension("dynamodb:index:ReadCapacityUnits")
            .serviceNamespace("dynamodb")
            .build());

    }
}
resources:
  dynamodbIndexReadTarget:
    type: aws:appautoscaling:Target
    name: dynamodb_index_read_target
    properties:
      maxCapacity: 100
      minCapacity: 5
      resourceId: table/${example.name}/index/${indexName}
      scalableDimension: dynamodb:index:ReadCapacityUnits
      serviceNamespace: dynamodb

The resourceId format extends to table/{table-name}/index/{index-name} for GSI scaling. The scalableDimension changes to dynamodb:index:ReadCapacityUnits to target the index specifically. This allows you to optimize costs and performance for specific query workloads without affecting the base table’s capacity.

Scale ECS service task count

Container workloads running in ECS need to scale task count based on metrics like CPU utilization or request count.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const ecsTarget = new aws.appautoscaling.Target("ecs_target", {
    maxCapacity: 4,
    minCapacity: 1,
    resourceId: `service/${example.name}/${exampleAwsEcsService.name}`,
    scalableDimension: "ecs:service:DesiredCount",
    serviceNamespace: "ecs",
});
import pulumi
import pulumi_aws as aws

ecs_target = aws.appautoscaling.Target("ecs_target",
    max_capacity=4,
    min_capacity=1,
    resource_id=f"service/{example['name']}/{example_aws_ecs_service['name']}",
    scalable_dimension="ecs:service:DesiredCount",
    service_namespace="ecs")
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/appautoscaling"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := appautoscaling.NewTarget(ctx, "ecs_target", &appautoscaling.TargetArgs{
			MaxCapacity:       pulumi.Int(4),
			MinCapacity:       pulumi.Int(1),
			ResourceId:        pulumi.Sprintf("service/%v/%v", example.Name, exampleAwsEcsService.Name),
			ScalableDimension: pulumi.String("ecs:service:DesiredCount"),
			ServiceNamespace:  pulumi.String("ecs"),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var ecsTarget = new Aws.AppAutoScaling.Target("ecs_target", new()
    {
        MaxCapacity = 4,
        MinCapacity = 1,
        ResourceId = $"service/{example.Name}/{exampleAwsEcsService.Name}",
        ScalableDimension = "ecs:service:DesiredCount",
        ServiceNamespace = "ecs",
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.appautoscaling.Target;
import com.pulumi.aws.appautoscaling.TargetArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var ecsTarget = new Target("ecsTarget", TargetArgs.builder()
            .maxCapacity(4)
            .minCapacity(1)
            .resourceId(String.format("service/%s/%s", example.name(),exampleAwsEcsService.name()))
            .scalableDimension("ecs:service:DesiredCount")
            .serviceNamespace("ecs")
            .build());

    }
}
resources:
  ecsTarget:
    type: aws:appautoscaling:Target
    name: ecs_target
    properties:
      maxCapacity: 4
      minCapacity: 1
      resourceId: service/${example.name}/${exampleAwsEcsService.name}
      scalableDimension: ecs:service:DesiredCount
      serviceNamespace: ecs

The resourceId follows the pattern service/{cluster-name}/{service-name}. The scalableDimension ecs:service:DesiredCount tells Application AutoScaling to adjust the number of running tasks. When paired with a scaling policy, this configuration automatically adds or removes tasks between 1 and 4 based on demand.

Scale Aurora read replica count

Read-heavy database workloads benefit from adding Aurora read replicas during peak hours and removing them during quiet periods.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const replicas = new aws.appautoscaling.Target("replicas", {
    serviceNamespace: "rds",
    scalableDimension: "rds:cluster:ReadReplicaCount",
    resourceId: `cluster:${example.id}`,
    minCapacity: 1,
    maxCapacity: 15,
});
import pulumi
import pulumi_aws as aws

replicas = aws.appautoscaling.Target("replicas",
    service_namespace="rds",
    scalable_dimension="rds:cluster:ReadReplicaCount",
    resource_id=f"cluster:{example['id']}",
    min_capacity=1,
    max_capacity=15)
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/appautoscaling"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := appautoscaling.NewTarget(ctx, "replicas", &appautoscaling.TargetArgs{
			ServiceNamespace:  pulumi.String("rds"),
			ScalableDimension: pulumi.String("rds:cluster:ReadReplicaCount"),
			ResourceId:        pulumi.Sprintf("cluster:%v", example.Id),
			MinCapacity:       pulumi.Int(1),
			MaxCapacity:       pulumi.Int(15),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var replicas = new Aws.AppAutoScaling.Target("replicas", new()
    {
        ServiceNamespace = "rds",
        ScalableDimension = "rds:cluster:ReadReplicaCount",
        ResourceId = $"cluster:{example.Id}",
        MinCapacity = 1,
        MaxCapacity = 15,
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.appautoscaling.Target;
import com.pulumi.aws.appautoscaling.TargetArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var replicas = new Target("replicas", TargetArgs.builder()
            .serviceNamespace("rds")
            .scalableDimension("rds:cluster:ReadReplicaCount")
            .resourceId(String.format("cluster:%s", example.id()))
            .minCapacity(1)
            .maxCapacity(15)
            .build());

    }
}
resources:
  replicas:
    type: aws:appautoscaling:Target
    properties:
      serviceNamespace: rds
      scalableDimension: rds:cluster:ReadReplicaCount
      resourceId: cluster:${example.id}
      minCapacity: 1
      maxCapacity: 15

The resourceId uses cluster:{cluster-id} to identify the Aurora cluster. The scalableDimension rds:cluster:ReadReplicaCount targets the number of read replicas. Application AutoScaling can add up to 15 replicas during high load and scale down to 1 during quiet periods, optimizing both performance and costs.

Scale MSK broker storage volume size

Kafka clusters running on MSK can automatically expand broker storage as data volume grows, preventing out-of-space errors.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const mskTarget = new aws.appautoscaling.Target("msk_target", {
    serviceNamespace: "kafka",
    scalableDimension: "kafka:broker-storage:VolumeSize",
    resourceId: example.arn,
    minCapacity: 1,
    maxCapacity: 8,
});
import pulumi
import pulumi_aws as aws

msk_target = aws.appautoscaling.Target("msk_target",
    service_namespace="kafka",
    scalable_dimension="kafka:broker-storage:VolumeSize",
    resource_id=example["arn"],
    min_capacity=1,
    max_capacity=8)
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/appautoscaling"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := appautoscaling.NewTarget(ctx, "msk_target", &appautoscaling.TargetArgs{
			ServiceNamespace:  pulumi.String("kafka"),
			ScalableDimension: pulumi.String("kafka:broker-storage:VolumeSize"),
			ResourceId:        pulumi.Any(example.Arn),
			MinCapacity:       pulumi.Int(1),
			MaxCapacity:       pulumi.Int(8),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var mskTarget = new Aws.AppAutoScaling.Target("msk_target", new()
    {
        ServiceNamespace = "kafka",
        ScalableDimension = "kafka:broker-storage:VolumeSize",
        ResourceId = example.Arn,
        MinCapacity = 1,
        MaxCapacity = 8,
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.appautoscaling.Target;
import com.pulumi.aws.appautoscaling.TargetArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var mskTarget = new Target("mskTarget", TargetArgs.builder()
            .serviceNamespace("kafka")
            .scalableDimension("kafka:broker-storage:VolumeSize")
            .resourceId(example.arn())
            .minCapacity(1)
            .maxCapacity(8)
            .build());

    }
}
resources:
  mskTarget:
    type: aws:appautoscaling:Target
    name: msk_target
    properties:
      serviceNamespace: kafka
      scalableDimension: kafka:broker-storage:VolumeSize
      resourceId: ${example.arn}
      minCapacity: 1
      maxCapacity: 8

The resourceId references the MSK cluster ARN directly. The scalableDimension kafka:broker-storage:VolumeSize targets broker storage capacity. Application AutoScaling expands storage from 1 to 8 units as needed, ensuring your Kafka cluster never runs out of space.

Beyond these examples

These snippets focus on specific scalable target features: DynamoDB table and index capacity scaling, ECS task count adjustment, and Aurora replica and MSK storage scaling. They’re intentionally minimal rather than full autoscaling solutions.

The examples reference pre-existing infrastructure such as DynamoDB tables and indexes, ECS clusters and services, and Aurora clusters and MSK clusters. They focus on registering scalable targets rather than provisioning the underlying resources.

To keep things focused, common scalable target patterns are omitted, including:

  • IAM role configuration (roleArn, usually auto-managed)
  • Suspended state controls (suspendedState)
  • Tagging and metadata (tags)
  • Scaling policies (configured via aws.appautoscaling.Policy)

These omissions are intentional: the goal is to illustrate how each scalable target is wired, not provide drop-in autoscaling modules. See the Application AutoScaling Target resource reference for all available configuration options.

Let's configure AWS Application Auto Scaling Targets

Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.

Try Pulumi Cloud for FREE

Frequently Asked Questions

Configuration & Immutability
What properties can't I change after creating a scalable target?
The resourceId, scalableDimension, and serviceNamespace properties are immutable and require resource replacement if changed.
What's the correct format for resourceId?

The format varies by service:

  • DynamoDB table: table/{tableName}
  • DynamoDB index: table/{tableName}/index/{indexName}
  • ECS service: service/{clusterName}/{serviceName}
  • RDS cluster: cluster:{clusterId}
  • MSK cluster: Use the cluster ARN directly
IAM & Permissions
Why is my custom IAM role being ignored?
Application AutoScaling defaults to an IAM Service-Linked Role for most service namespaces, and custom IAM roles specified in roleArn are ignored by the API for those namespaces.
Does Application AutoScaling automatically create IAM roles?
Yes, the service automatically attempts to manage IAM Service-Linked Roles when registering certain service namespaces for the first time. You can manually manage this role using aws.iam.ServiceLinkedRole if needed.
Tags & Lifecycle
Why am I seeing perpetual diffs with tags on my scalable target?
Scalable targets created before 2023-03-20 may not have an assigned arn and cannot use tags or participate in defaultTags. Use lifecycle.ignore_changes to suppress these differences that can never be reconciled.
How do I suppress tagsAll differences for older scalable targets?
Add lifecycle.ignore_changes to your resource configuration to prevent Pulumi from showing tag-related diffs for targets created before 2023-03-20.

Using a different cloud?

Explore compute guides for other cloud providers: