Skip to content

AWS::DynamoDB::GlobalTable: initial capacity derives from SeedCapacity where CloudFormation uses MinCapacity (silent over-provision on create) #1435

Description

@go-to-k

Found by the live CloudFormation probe run for #1427 (see that comment for the full probe record).

Summary

deriveReadCapacityUnits / deriveWriteCapacityUnits in src/provisioning/providers/dynamodb-globaltable-provider.ts both end with:

return toFiniteNumber(autoScaling['SeedCapacity']) ?? toFiniteNumber(autoScaling['MinCapacity']);

SeedCapacity first. CloudFormation uses MinCapacity on a fresh CREATE, so cdkd over-provisions any autoscaled PROVISIONED GlobalTable by the seed-to-min ratio. It is a billing symptom, not an error: the deploy succeeds and the table is simply more expensive than the same template under CloudFormation.

Evidence (real AWS, us-east-1)

Stack CdkdIssue1427Control, a AWS::DynamoDB::GlobalTable with BillingMode: PROVISIONED and, at table level and on the GSI:

WriteProvisionedThroughputSettings:
  WriteCapacityAutoScalingSettings:
    MinCapacity: 1
    MaxCapacity: 30
    SeedCapacity: 20
    TargetTrackingScalingPolicyConfiguration:
      TargetValue: 70

CREATE_COMPLETE, then dynamodb describe-table:

{"Table": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 1, "NumberOfDecreasesToday": 0},
 "GSI": [{"Name": "gsi1", "PT": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 1, "NumberOfDecreasesToday": 0}}]}

CloudFormation created at 1 (MinCapacity), not 20 (SeedCapacity). cdkd's helpers return 20 for the same input.

NumberOfDecreasesToday: 0 rules out an auto-scaling scale-down between create and read-back, and application-autoscaling describe-scalable-targets confirms Min: 1 / Max: 30 registered on both dynamodb:table:WriteCapacityUnits and dynamodb:index:WriteCapacityUnits.

Why CloudFormation is right here

The CapacityAutoScalingSettings docs scope SeedCapacity to the billing-mode transition, not to a fresh provisioned create:

When switching billing mode from PAY_PER_REQUEST to PROVISIONED, DynamoDB requires you to specify read and write capacity unit values for the table and for each global secondary index. [...] The table will use these provisioned values until CloudFormation creates the autoscaling policies you configured in your template.

You must also specify a value for SeedCapacity when you plan to switch a table's billing mode from PROVISIONED to PAY_PER_REQUEST, because CloudFormation might need to roll back the operation [...]

MinCapacity / MaxCapacity are Required: Yes; SeedCapacity is Required: No. So the correct precedence is context-dependent, not a fixed ?? chain:

  • fresh CREATE of a PROVISIONED table -> MinCapacity
  • PAY_PER_REQUEST -> PROVISIONED flip -> SeedCapacity (falling back to MinCapacity when absent)

Do NOT fix this in isolation

Flipping the order alone makes things worse. Per #1419, create() registers no Application Auto Scaling target at all, so a table created at MinCapacity would be pinned at MinCapacity forever with no policy to scale it up — strictly worse than today's over-provision. Today's SeedCapacity-first behavior is accidentally acting as a safety margin for the missing registration.

The two belong in one change: register the scaling targets/policies on create() (#1419) and seed the initial capacity from MinCapacity, so the table starts where CloudFormation starts it and the policy takes over from there.

Scope

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions