Expand description
Read dynamic-config configuration from an S3 object.
The AWS SDK is async throughout, so this implements the async
AsyncRemoteSource trait rather than the blocking one.
use dynamic_config_s3::S3;
// Credentials come from the environment the way every other AWS tool finds
// them: variables, the profile, the instance role, IRSA.
DbConfig::set_remote_async(S3::new("myapp-config", "prod/db.json").await?);
// Fetching is explicit; the load that follows touches no network.
DbConfig::refresh_remote_async().await?;§What it reads
One object, whose body is a whole configuration document — the same
bytes that would be in a config file. The format comes from the key’s
extension, or from with_format.
§Several objects as one document
A deployment that splits its configuration across a prefix —
prod/db.json, prod/server.json — can have one source read the lot, and
Keys says which:
// Named keys: a list of layers, merged in the order given, later wins.
let s3 = S3::new("myapp-config", Keys::several(["prod/base.json", "prod/local.json"])).await?;
// A prefix: disjoint sections, and an overlap between two of them is an error.
let s3 = S3::new("myapp-config", Keys::prefix("prod/"))
.await?
.with_format(dynamic_config::Format::Json);Neither shape is atomic, and S3 offers nothing that would make one so.
There is no batch read: a named list is one GetObject per key, and a
prefix is one ListObjectsV2 and then one GetObject per key it named. A
write landing between two of those requests can produce a document that
never existed as a whole. AWS made ListObjectsV2 strongly consistent in
December 2020, so the listing itself is not the hole it once was — but
another implementation of this API is free to be eventually consistent, and
the gap between the listing and the reads is there in every case.
The 512-key bound is applied to the listing, not to what it fetched.
ListObjectsV2 is paginated, so the count is checked as each page arrives
and a prefix over a bucket of a million objects is refused after one
request rather than after a million bodies. Every key the store answers
with is checked against the literal prefix, and a key ending in / — the
zero-byte object the console makes when somebody creates a “folder” — is
not a document and is skipped.
Three consequences that belong here rather than in an incident:
- Provenance becomes store-grained. The merged document is one layer,
so
source_ofanswers “from s3 … keys a, b” rather than naming which key supplied a value.describenames the whole set, which is as close as one layer gets. - One unreadable key fails the whole fetch. A configuration quietly missing a section is worse than a refresh that failed and left the last document serving.
- A multi-key source cannot be watched. What a watch delivers is the
object that changed, and a merged document has no one ETag; it refuses at
watchand points at pollingrefresh_remote_async().
§Credentials
Through aws-config, which is the chain every AWS tool uses:
AWS_ACCESS_KEY_ID, the shared profile, the EC2 instance role, the ECS task
role, and IRSA on EKS. That is deliberately not re-implemented here — a
second credential chain in a program that already has one is a bug waiting
for a rotation.
with_config takes an SdkConfig the program already
built, which is also how a non-AWS endpoint is reached: MinIO, Ceph,
Cloudflare R2, Backblaze B2 all speak this API.
§Watching
S3 cannot tell you when an object changes without a notification pipeline —
SNS, SQS, EventBridge — that is a deployment’s decision, not a library’s. So
watch polls, and says so.
What it does not do is download the object every tick. HEAD returns the
ETag, which changes when the body does, so an unchanged configuration costs
one small request and no transfer.
A failing poll says so, if it is asked to.
reporting_to hands the loop the same sink it delivers
through, and the failures inside it — a HEAD that did not answer, and a
GET that did not answer after the ETag moved — are reported to the
RemoteStatus as they happen. Surviving a failure is what makes that
necessary: a loop that retries forever is a loop that reports nothing
forever, so dynamic_config_remote_up would describe the last delivery
rather than the last attempt.
A credential the store will not accept — AccessDenied,
InvalidAccessKeyId, SignatureDoesNotMatch, an expired session token —
is reported as ErrorKind::Auth rather than Remote, because no amount of
waiting persuades S3 otherwise. A clock too far out of step
(RequestTimeTooSkewed) shares the same 403 and stays Remote: that one
does come right.
§Timeouts
S3::with_timeout is the deadline for a single fetch attempt, excluding
retries the underlying client performs. Here that exclusion has teeth: the
SDK retries, so a five-second timeout with three attempts is a
fifteen-second call. See the README’s Timeouts section.
§This crate needs a tokio runtime
Not this crate’s choice: the AWS SDK it is built on is tokio-based
(rt-tokio), and watch sleeps on tokio’s timer. Driving
it from another executor panics inside the SDK. The etcd and NATS
companions are executor-agnostic; this one is honest about not being.
§Every failure branch of the watch loop, and what it reports
A watch is the half of a store dynamic-config cannot see, and
reporting_to is what lets it speak: the sink the
loop already holds is told about every attempt that came back with
nothing. Which attempts those are is a table rather than prose, because
the question an operator asks is which silence is deliberate.
Three rules decide the column, and they are the same three in all seven store crates:
- A failure the loop survives by retrying reports. That is the case the whole feature exists for: the stream is down, the last delivery is old, and nothing else would ever say so out loud.
- A recovery that worked stays silent. Only a delivery or a fetch
clears the streak, so reporting a five-minute token turning over on a
healthy cluster would drive
remote_upto zero and leave it there. - A refusal that never asked the store reports nowhere. No format, a
key shape that cannot be watched, material that will not build a
client:
RemoteStatus::reachable()is whether the store answered the last time it was asked, and these never ask. They are returned to the caller, who is the one holding the mistake — and a status cannot correct them, since it carries a kind and a path and no message.
| Branch | Reports |
|---|---|
| the format is missing, or the source names several keys | no — rule 3: nothing has been asked of the bucket |
the HEAD that reads the ETag fails — an expired credential, a bucket briefly unreachable | yes, and the loop waits out the interval |
the GET after a moved tag fails | yes, and seen is left where it was so the next tick tries the same tag again |
| the first tick, or an ETag that has not moved | no — the bucket answered |
on_change refuses the document | no — the bucket answered; apply counted the delivery, and what the document did next is ConfigStatus’s half |
Structs§
- Client
- Client for Amazon Simple Storage Service
- S3
- An object in S3, as a configuration source.
- SdkConfig
- The AWS types a caller needs to configure this, re-exported so using them needs no direct dependency on the SDK. AWS Shared Configuration
- TlsConfig
- A private certificate authority and a client certificate, as data.
Enums§
- Keys
- What a source reads: one object, several named ones, or a prefix.