AWS Secrets Manager resolver

Config resolver that swaps aws-secret: placeholders for live values fetched from AWS Secrets Manager.

Overview

Piko's config loader resolves placeholder strings of the form <prefix>:<value> against any registered resolver. This resolver handles the aws-secret: prefix. It implements the same config.Resolver interface as the built-in env:, file:, and base64: resolvers, so it slots into the exact same registration path with no glue code. At config load time, the loader walks the config struct, spots the placeholder anywhere a string field allows, calls Secrets Manager via the AWS SDK v2, and substitutes the result. Resolution happens once during startup, so a missing secret or a denied IAM call fails the boot at config load, not on a later request.

To read a single field from a JSON-shaped secret, suffix the secret ID with #<key>, for example aws-secret:prod/api-keys#stripe. The lookup reads one top-level key only, with no nested paths. The resolver renders the matched value as a string, so a JSON number or boolean arrives as its text form.

The resolver uses the standard AWS credential chain from config.LoadDefaultConfig. That chain covers environment variables, the shared config and credentials files, an IAM Identity Center profile, and an EC2, ECS, or Lambda role. You supply no static credentials at construction. The config loader wraps each lookup in a circuit breaker, so a failing Secrets Manager endpoint does not stall every other resolver.

Requirements

  • AWS credentials available via the standard chain at startup.
  • IAM permission secretsmanager:GetSecretValue on the secrets you reference. If a customer-managed KMS key encrypts the secret, also kms:Decrypt against that key.
  • Network egress to the regional Secrets Manager endpoint (for example secretsmanager.eu-west-1.amazonaws.com).

Each distinct aws-secret: placeholder costs one GetSecretValue call at startup. The resolver does not batch, so plan the referenced secret count against IAM throttling and cold-start latency.

The package is pure Go and imports only the AWS SDK v2. It carries no build tags and no CGO, so it behaves the same under a compiled binary and under the interpreted dev-i run mode, with no extra run-mode handling.

Configuration

NewResolver takes a context.Context because the AWS SDK loads its configuration with one (used during credential resolution).

import (
    "context"

    "piko.sh/piko/wdk/config/config_resolver_aws"
)

resolver, err := config_resolver_aws.NewResolver(ctx)
if err != nil {
    return err
}

The resolver advertises aws-secret: as its prefix. Reference secrets in your config struct using that prefix:

database:
  password: aws-secret:prod/db/main
stripe:
  secret_key: aws-secret:prod/api-keys#stripe

Bootstrap

Resolvers register through piko.WithConfigResolvers. The option is variadic, so you pass one resolver or a list in a single call.

import (
    "piko.sh/piko"
    "piko.sh/piko/wdk/config/config_resolver_aws"
)

resolver, err := config_resolver_aws.NewResolver(ctx)
if err != nil {
    return err
}

ssr := piko.New(
    piko.WithConfigResolvers(resolver),
)

The package also exports a Register(ctx) shortcut. It constructs a resolver and registers it in the global resolver registry, which suits an init() in a library that wants a resolver available before it calls piko.New. Piko's bootstrap sets UseGlobalResolvers to true, so the loader picks up every globally registered resolver without an explicit WithConfigResolvers call:

func init() {
    if err := config_resolver_aws.Register(context.Background()); err != nil {
        log.Fatal(err)
    }
}

See also

Sibling resolvers:

Companion loader:

Framework docs:

External: