Skip to main content

Client integration

For anyone building a consumer that is not the existing Spark path. Four rules carry almost all of the risk; the rest is detail.

1. Resolve once, on one coordinating process​

Resolve on whatever single process coordinates the work — a Spark driver, a job scheduler, a CLI entry point — and distribute the resolved envelope to workers. Cache it until expiresAt, then resolve again.

Do not let every worker resolve for itself. It multiplies load on __admin and on the RDS describe APIs, which are low-rate, and it turns a job start into a burst of identical requests. The Locator keeps a small in-memory cache that absorbs repeats within one warm instance, but that does nothing for a cold burst and should not be relied upon.

For Spark specifically, the format("depot") wiring resolves on the driver and serialises the result into JDBC options, which is the shape to copy. Routing a raw depot: URL through a per-connection JDBC provider instead pushes resolution onto every executor — avoid that shape.

2. Sign the token against the canonical endpoint, never the connection host​

For credential.kind === 'RDS_IAM_TOKEN' the consumer mints its own token. It must be signed against canonicalEndpoint, regardless of the host actually being connected to.

This separation is what makes VPC endpoints and tunnels work at all: the connection may go to an interface-endpoint alias or to 127.0.0.1:<local port>, while the signature must still name the real database. Getting this wrong produces an authentication failure that looks like a permissions problem.

Sign as whichever DB user you have decided to be — the Locator names none, and holds no opinion. The partition, account and region come from the depot: URL you already parsed. If your driver wants these as a property bag, build one; connectionProperties carries only what the store determines (TLS, keep-alive, read-only intent), so you can pass it through and add your own entries alongside.

3. Branch on reachability, and own the tunnel (Aurora)​

ValueWhat to do
DIRECTUse underlyingUrl as given. The Locator knows of no indirection — you may still need to probe.
VPC_ENDPOINTUse underlyingUrl; it already names the interface endpoint in your VPC.
REQUIRES_TUNNELOpen your own tunnel, then replace the host and port in underlyingUrl with its local end. Leave connectionProperties alone.

Pass callerVpcId when the consumer might be in a different VPC from the database — read it from instance metadata, which costs no permission. Being in the database's own VPC is fine to declare: the Locator recognises it and answers DIRECT. But omit it if you reach the database by peering or transit routing, because the Locator cannot see routing and would tell you to tunnel needlessly.

Pass needsBastionHint: true only for development clients prepared to run aws ssm start-session.

4. Handle throttling, in whatever form your Lambda client surfaces it​

Two distinct conditions, and they want different responses:

  • Your invoke was throttled. Surfaces as HTTP 429 / TooManyRequestsException, possibly with a Retry-After. Retry with exponential backoff and full jitter — jitter is not optional, because a synchronised burst retrying on a fixed delay re-synchronises. The AWS SDK already retries throttling internally, so your retry is the outer layer; stacking aggressive retries multiplies the burst that caused the throttle.
  • The Locator's own dependency was throttled. Surfaces as outcome: 'ERROR' with error.kind === 'DOWNSTREAM_THROTTLED', retryable: true, and sometimes retryAfterSeconds.

Every other error.kind — NOT_FOUND, INVALID_REQUEST, UNSUPPORTED_LOCATION, DOWNSTREAM_ERROR — carries retryable: false. Branch on retryable, not on the message.

Handling the credential​

For RDS_IAM_TOKEN there is nothing to handle: no secret is sent.

For SECRETS_MANAGER, the response carries only a pointer — credential.secretArn — never the secret itself. Reading it, and turning it into driver options, is the caller's own job.

The secret's shape. Nothing upstream of the caller validates it anymore, so read it defensively. It is JSON with these optional fields, all lowercase — privatekey, not privateKey; getting the case wrong makes the field read as absent:

interface SnowflakeCredentialSecret {
username?: string;
password?: string;
privatekey?: string;
region?: string;
warehouse?: string;
}

Derive the driver option from it in this order: if privatekey is present, write it to pem_private_key; else if password is present, write it to sfPassword; else the secret is unusable for this purpose — fail loudly rather than connecting with neither.

const secret: SnowflakeCredentialSecret = JSON.parse(
(await secretsManager.getSecretValue({ SecretId: response.credential.secretArn })).SecretString!,
);
const options = { ...response.connectionProperties };
if (secret.privatekey) {
options.pem_private_key = secret.privatekey;
} else if (secret.password) {
options.sfPassword = secret.password;
} else {
throw new Error('Snowflake credential secret has neither privatekey nor password');
}

The connect-as username is the secret's own username field — there is no override to consult. ResolvedCredential's SECRETS_MANAGER member carries no username of its own, and no deployed Depot environment declares an executor-level override, so there is no precedence rule to apply here. Treat the secret's username being absent as a data error, not a silent fallback.

Reading the secret is a default for the common case, not the only option. A caller with its own way to authenticate to Snowflake — a personal access token, externalbrowser SSO, or any credential an operator already holds for interactive or ad hoc scripts — may disregard credential and secretArn entirely and authenticate however it already does. The rest of the envelope (connectionProperties.sfUrl, sfWarehouse when known) is still useful on its own; secretArn merely names a credential the caller is welcome to use, not the only one that will work.

connectionProperties never contains a secret, which means it is safe to log — and the credential itself is never safe to log. Keeping the secret out of the response entirely (rather than in one named field) is what makes that distinction structural now, instead of depending on a naming convention.

Connection labelling and query tags are yours​

The Locator never opens a connection, so anything that labels one belongs to you:

  • Aurora's ApplicationName — set it when you assemble connection properties. The Locator does not emit it. A port that expects it in the envelope will silently lose session attribution in Postgres.
  • Snowflake's query_tag and cost tags — applied per query by the caller, not part of resolution.

Keeping these out of the request also keeps them out of the cache key, avoiding a class of bug where two resolutions differing only by a label share an entry.

The request's applicationName is not this. It is a self-asserted label used only in the Locator's log line, because a directly-invoked lambda cannot see who invoked it. It is required: send something short and stable — it is sanitised and truncated to 40 characters — and never rely on it for anything but forensics. Omitting it, or sending something that sanitises to nothing, is INVALID_REQUEST.

Snowflake: authenticate as the secret's user​

For Snowflake, connect as the credential secret's own username field — the Locator emits no sfUser connection property for this kind, and names no user of its own. Using it would authenticate as an identity the key was never issued for.

See "Handling the credential" above for reading the secret; the username you read from it there is the same one to connect as.

sfUrl embeds the Snowflake account's region, not your AWS region. The two happen to match in current deployments, but the account-locator URL form is region-specific and does not redirect, so the Locator prefers the region declared for the credentials over the caller's AWS region.

Writing​

Pass readOnly: false only with a schema, and only for a record schema. Anything else, including an ordinary object table, is refused with INVALID_REQUEST. See "Asking to write" in the overview for why the gate is this narrow. readOnly coming back in connectionProperties is the Locator's assent rather than an echo of your request.

Authorisation is coarse today​

Any principal that can invoke the Locator can resolve any dataset in the environment, and receive whatever credential that entails. Grant invoke rights narrowly and deliberately.

Same-account consumers should grant themselves lambda:InvokeFunction in their own stack, addressing the function through gatewayEndpoints.storageLocatorArn. That keeps a service's requirements declared where the service is defined, rather than in an environment definition it may not own. The environment-level locator.invokePrincipals list is for cross-account callers only, where a resource policy is the only mechanism available.

Per-dataset authorisation is designed but not built; the reasoning and the options are recorded against DPT-2934.