Provider reference
This page is the lookup companion to Provider Development, which walks through writing a provider. Come here to find out what an interface member is for, to check your provider against the rules before you open a pull request, or to fix a provider that does not behave as you expect.
The ResourceProvider interface
Every provider implements ResourceProvider. The interface is declared in
src/types/resource.ts,
and that file documents every member.
create, update and delete are required. Every other member is optional,
and you implement it only when your resource type needs it:
| Member | Implement it when |
|---|---|
handledProperties |
Always, in practice. It routes resources to you and feeds the coverage test. |
unhandledByDesign |
A schema property is deliberately not sent |
import |
The type should be adoptable by cdkd import without an explicit id |
readCurrentState |
The type should take part in cdkd drift |
getAttribute |
An attribute may need a live read because state does not hold it |
removalDefaults, removalHandledInUpdate |
A removed property resets to a constant, or update already handles the removal |
getDriftUnknownPaths, getDriftUnorderedPaths, canonicalizeDriftProperties, canonicalizeDriftPair |
AWS reads a property back differently from how it was sent |
canonicalizeDesiredProperties |
The provider sends something narrower than what the template declares |
disableCcApiFallback |
Cloud Control cannot manage the type, so an unhandled property must be an error |
disableOuterRetry |
A retry by the engine would break per-call state the provider prepares |
getMinResourceTimeoutMs |
An operation on the type can outlast the default per-resource timeout |
isSameResource, resourceIdentity |
A failed create can leave an orphan under a name that can be reused |
create and update can also return two less common fields,
effectiveProperties and noEchoAttributes.
Provider Development internals describes
those two fields, the canonicalizers and the orphan identity methods.
Rules every provider obeys
Go through this checklist before you open a pull request. The walkthrough shows most of the rules in code. Each rule below links to its full statement, and the second column says what goes wrong when the rule is broken.
Errors and input
| Rule | What goes wrong otherwise |
|---|---|
Wrap every AWS call and rethrow as ProvisioningError, with the original error as the cause |
The failure reaches the user without the resource it belongs to |
| Refuse a malformed value before any call, and never infer a default from one | A typo deploys as a default nobody chose |
| Respect the service's name length and character limits | Create fails for a long stack or logical id |
| Send an idempotency token when create mints a server-side id | A retried create makes a second resource |
Delete
| Rule | What goes wrong otherwise |
|---|---|
Treat "not found" on delete as success only after assertRegionMatch |
A destroy in the wrong region empties state and orphans the resources |
| Report a delete that made no AWS call as skipped | State drops a resource that is still running |
Update
| Rule | What goes wrong otherwise |
|---|---|
| Reset a property the template removed | AWS keeps the old value while cdkd diff reports no changes |
| Do not erase AWS-authored values with a full-replace update | An update deletes configuration AWS or the service added |
Attributes, properties and drift
| Rule | What goes wrong otherwise |
|---|---|
Return every Fn::GetAtt attribute, and build ARNs from the partition |
References fail, or are wrong outside the aws partition |
Leave out an attribute you could not read. Never store '' for it. |
Fn::GetAtt resolves to an empty string |
Emit every user-settable property from readCurrentState |
A change made in the console is invisible to drift |
List every sent property in handledProperties |
The resource is routed to Cloud Control, or a property is dropped |
Region, startup and logging
| Rule | What goes wrong otherwise |
|---|---|
Take the region from ambientRegion() and credentials from getAwsClients() or ambientClientDefaults() |
A multi-region deploy calls the wrong region |
Register only in registerAllProviders, and never import a provider module statically |
Every command loads the SDK client at startup |
Pass any log line that includes a resolved property value through context.maskSecrets, which create and update receive |
A resolved secret is printed |
A retry loop inside a provider must honor Ctrl-C
Most providers need no retry loop, because the deploy engine retries a failed
provider call itself. This rule applies when a provider calls withRetry
(src/deployment/retry.ts) on its own.
The engine checks for an interrupt between operations, and withRetry is the
only wait that checks during a backoff. But withRetry can only check when it
is given an interrupt watch. A provider that omits the watch leaves
Ctrlc with no effect until the retries run out. Pass the watch like
this:
const watch = startInterruptWatch(`... ${logicalId}`);
try {
await withRetry(op, logicalId, {
logger: this.logger,
isInterrupted: watch.isInterrupted,
onInterrupted: watch.onInterrupted,
});
} finally {
watch.dispose();
}
startInterruptWatch is in src/provisioning/interrupt-watch.ts. The task
vp run audit:withretry-interrupt:check fails on a withRetry call under
src/provisioning/ that does not pass both options.
A create that adopts an existing name
Some create APIs do not fail when the name is taken. They hand back the
existing resource, or overwrite it. CreateQueue, CreateTopic,
PutMetricAlarm and PutRule behave this way. A deploy must not record
another deployment's resource as its own, so such a type needs two more
methods.
Add the type to NAME_ADOPTING_SDK_CREATE_TYPES
(src/deployment/replacement-name-holder/deploy-name.ts) and implement:
| Method | Returns |
|---|---|
generatedCreateName(resourceType, logicalId, properties) |
The name create() sends when the template declares none, and undefined when it declares one |
lookupNames(resourceType, names, ctx) |
A Map<name, physicalId> of the names a resource already holds |
Three rules keep the check sound:
create()callsgeneratedCreateName, so the name it sends and the name cdkd looks up cannot drift apart.lookupNamesreads by exact name, in a batch call where the service has one and otherwise one read per name. It never lists: a listing is eventually consistent and can omit a resource created a moment ago.- A resource that is being deleted reads as absent, and each API call
goes through
withApiLimit(src/provisioning/name-lookup.ts).
Without lookupNames, cdkd calls import() once per name. If the lookup
needs a property the plan cannot know yet, also implement
lookupNeedsResolvedProperties.
A deploy then refuses a create whose generated name another resource holds,
unless the stack's own records name that resource. See
Name check before a create.
tests/unit/deployment/generated-name-guard-types-4705.test.ts checks that
every listed type has both methods.
A provider that waits for stabilization
Some creates return before the resource is usable, so the provider polls until
the resource is ready. A user can change the waits with --no-wait and
--full-wait, which a provider sees as the environment variables
CDKD_NO_WAIT and CDKD_FULL_WAIT.
If your provider skips or lengthens its wait under either variable, document
the behaviour in two places: the wait table in cdkd deploy, and
the option's help text in src/cli/options.ts.
Custom resources
You do not write a provider for a custom resource type, because the handler is the user's own Lambda function. One existing provider serves all of them.
CustomResourceProvider
(src/provisioning/providers/custom-resource-provider.ts) handles Custom::*
and AWS::CloudFormation::CustomResource. It sends the handler the same
request CloudFormation would, with RequestType set to Create, Update or
Delete. From the handler's response it takes the physical id
(PhysicalResourceId) and the attributes (Data).
When something does not work
Your provider is not called
The resource went to Cloud Control. Run cdkd diff MyStack to see why: a
resource that was routed there because of a property is annotated
[via CC API: <property>]. There are three possible causes:
- the type is not registered,
- the template sets a property that is missing from
handledProperties, - state already records the resource as created through Cloud Control.
Fn::GetAtt does not resolve
create or update did not return the attribute. Add it to the attributes
map both methods return, under the name CloudFormation documents.
An update fails on a property that cannot change
The property is create-only, and the diff did not classify the change as a
replacement. The diff reads the type's entry in
src/analyzer/replacement-rules.ts first. For a property that entry does not
classify, it falls back to the createOnlyProperties of the type's
CloudFormation schema. Check both.
Related
- Provider Development: the walkthrough
- Provider implementation rules: the complete rulebook
- Provider Development internals: the less common return fields and methods