Skip to content

Allow aiodynamo to fetch credentials for snapstart lambdas.#196

Open
aclemons wants to merge 1 commit intoHENNGE:masterfrom
aclemons:snapstart-compat
Open

Allow aiodynamo to fetch credentials for snapstart lambdas.#196
aclemons wants to merge 1 commit intoHENNGE:masterfrom
aclemons:snapstart-compat

Conversation

@aclemons
Copy link
Contributor

@aclemons aclemons commented Dec 6, 2024

For snapstart, the lambda boots once to do any static init.
Subsequently, this snapshotted version is used for handling subsequent
requests.

Normally for lambdas, the AWS credentials are in the environment. Since
snapstart will reuse a snapshot of the running application, it can't
pass in variables in this way since they need to be refreshed.

Instead, the AWS_CONTAINER_CREDENTIALS_FULL_URI env var is set and the
ContainerMetadataCredentials in aiodynamo should be used to get
credentials if using Credentials.auto() to initialise.

Unfortunately, it looks like AWS formats the expiration timestamps
slightly differently for this API on lambda, including the microseconds
now. The parse_amazon_timestamp function fails to parse the value,
resulting in ContainerMetadataCredentials being rejected as a
ChainCredentials candidate and we end up with no credentials at all.

I can't really find a definitive documentation of what the format should
be so I can point to it, but obviously we know the code as it is works
on ECS/EC2 etc so we must continue to be able to parse those. I've
simply added a fallback to use microseconds if the string has a
full-stop in it.

It seems botocore is using the dateutil package to handle their
parsing:

https://github.com/boto/botocore/blob/f49ead849aa5a4ea428d9f378de14db6f4c6d645/botocore/utils.py#L950

For snapstart, the lambda boots once to do any static init.
Subsequently, this snapshotted version is used for handling subsequent
requests.

Normally for lambdas, the AWS credentials are in the environment. Since
snapstart will reuse a snapshot of the running application, it can't
pass in variables in this way since they need to be refreshed.

Instead, the `AWS_CONTAINER_CREDENTIALS_FULL_URI` env var is set and the
`ContainerMetadataCredentials` in aiodynamo should be used to get
credentials if using `Credentials.auto()` to initialise.

Unfortunately, it looks like AWS formats the expiration timestamps
slightly differently for this API on lambda, including the microseconds
now. The `parse_amazon_timestamp` function fails to parse the value,
resulting in `ContainerMetadataCredentials` being rejected as a
`ChainCredentials` candidate and we end up with no credentials at all.

I can't really find a definitive documentation of what the format should
be so I can point to it, but obviously we know the code as it is works
on ECS/EC2 etc so we must continue to be able to parse those. I've
simply added a fallback to use microseconds if the string has a
full-stop in it.

It seems botocore is using the `dateutil` package to handle their
parsing:

https://github.com/boto/botocore/blob/f49ead849aa5a4ea428d9f378de14db6f4c6d645/botocore/utils.py#L950
Copy link
Contributor

@dimaqq dimaqq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change make sense, but feels like it's only the first step in supporting snapstarts?

Or did I misread the PR description?

tzinfo=datetime.timezone.utc
)
if "." in timestamp:
value = datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, Python's datetime parsing thing can parse fractional seconds, but only with three or six fractional digits exactly:

  • 59.123 ok
  • 59.120 ok
  • 59.12 Error

Please collect more test vectors and/or add add more test inputs to cover this corner case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parser will handle any length from 1 to 6, it just right pads the values. i.e. if AWS were to put 59.12 in the Expiration field of the credential response it will be interpreted as 59.120000

The docs are here:

https://docs.python.org/3/library/datetime.html#format-codes

Directive Meaning Example Notes
%f Microsecond as a decimal number, zero-padded to 6 digits. 000000, 000001, …, 999999 (5)
  1. When used with the strptime() method, the %f directive accepts from one to six digits and zero pads on the right. %f is an extension to the set of format characters in the C standard (but implemented separately in datetime objects, and therefore always available).

@dimaqq
Copy link
Contributor

dimaqq commented Jun 11, 2025

cc @ojii

@aclemons
Copy link
Contributor Author

The change make sense, but feels like it's only the first step in supporting snapstarts?

Or did I misread the PR description?

I think you misread.

Normally for lambdas, the AWS credentials are in the environment. Since
snapstart will reuse a snapshot of the running application, it can't
pass in variables in this way since they need to be refreshed.

Instead, the AWS_CONTAINER_CREDENTIALS_FULL_URI env var is set and the
ContainerMetadataCredentials in aiodynamo should be used to get
credentials if using Credentials.auto() to initialise.

In a snapstart lambda, the AWS_CONTAINER_CREDENTIALS_FULL_URI env var is set and aiodynamo tries to use ContainerMetadataCredentials but cannot parse the expiration field in the response. Once the timestamp parsing is fixed, it can correctly use the provider and everything else works as it does on a normal lambda.

We've been using aiodynamo for a production snapstart lambda for about 6 or 7 months now with the parsing function monkey patched to handle the microseconds.

Copy link
Contributor

@dimaqq dimaqq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change looks good for me.

Tested manually on py3.8 ~ 3.14.

What works:

  • without fractional part (old code path)
  • with fractional part, from .1 to .123456

What doesn't work:

  • ...01:02:03. (with a dot but without the fractional part): this is fair
  • ...01:02:03.1234567 and longer: I guess that's OK?

@dimaqq
Copy link
Contributor

dimaqq commented Dec 4, 2025

cc @ojii

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants