Skip to content

Share one credential across components

You are using more than one AWS-backed component — configuration from Parameter Store, files from S3, signing through KMS — and you want them to resolve the AWS credential chain once between them.

Build one source, inject it everywhere

import (
    "gitlab.com/phpboyscout/go/awsclient"
    "gitlab.com/phpboyscout/go/config"
    s3ambient "gitlab.com/phpboyscout/go/config-aws-s3/ambient"
    ssmambient "gitlab.com/phpboyscout/go/config-aws-ssm/ambient"
)

src := awsclient.Ambient(awsclient.WithRegion("eu-west-2"))

paramStore, err := ssmambient.FromSource(ctx, src, "/app")
if err != nil {
    return err
}

bucket, err := s3ambient.FSFromSource(ctx, src, "my-bucket")
if err != nil {
    return err
}

store, err := config.NewStore(ctx,
    config.WithBackend(paramStore),
    config.WithFiles(bucket, "config.yaml"),
)

Note the import paths: the FromSource rung lives in each adapter's ambient subpackage, not its root. That is deliberate — the root package must not carry the AWS credential-resolution graph, and keeping that boundary is why this module exists as a separate import at all.

One chain resolution, two adapters. The same source can feed go/signing and go/encryption — it is an estate module, not a config one.

What you are trading away

Without this, each adapter resolves its own chain. That is the default, and it is deliberate: components stay isolated, and one component's transient credential failure is not everybody's.

Sharing gives you one resolution and one failure domain. Both are the point — but they arrive together, so take the source-sharing route because you want the shared failure domain too, not merely to save a resolution.

Overriding what the chain resolves

WithLoadOptions passes the SDK's own options straight through, so anything config.LoadDefaultConfig accepts is available:

src := awsclient.Ambient(
    awsclient.WithRegion("eu-west-2"),
    awsclient.WithLoadOptions(
        awscfg.WithSharedConfigProfile("build"),
    ),
)

When you already have an aws.Config

If your process resolves AWS configuration for its own reasons, inject it rather than resolving a second time:

src, err := awsclient.FromConfig(cfg)

That rung validates and returns any error immediately, because there is nothing to defer — the configuration already exists.

Bounding the resolution

src := awsclient.Ambient(
    awsclient.WithRegion("eu-west-2"),
    awsclient.WithBuildTimeout(5*time.Second),
)

Per attempt, not a total budget. The bound is cooperative: it cancels the context the SDK is given, and a loader that ignores it runs to completion regardless.

Shutting down

WithLifetimeContext ties resolution attempts to the life of whatever owns the source, so shutting that down abandons an attempt in flight:

src := awsclient.Ambient(
    awsclient.WithRegion("eu-west-2"),
    awsclient.WithLifetimeContext(appCtx),
)

It is deliberately not a call's context — a caller's cancellation must release that caller alone.