configawsssm

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 11 Imported by: 0

README

config-aws-ssm

Read AWS SSM Parameter Store as a config layer through config

Go Reference Pipeline Coverage phpboyscout Go toolkit

Part of the phpboyscout Go toolkit — small, framework-free Go modules extracted from go-tool-base. Documented with the parent module at config.go.phpboyscout.uk


config reads files. A remote system like AWS Systems Manager Parameter Store is a sibling backend module like this one, so a consumer who configures from Parameter Store takes it and one who does not pays nothing for it.

You build and configure the SSM client — that is where every Region, credential, endpoint and KMS-key decision lives — and hand it in. The adapter takes a prefix that scopes and is stripped from the parameter names:

import (
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/ssm"
	cfg "gitlab.com/phpboyscout/go/config"
	configawsssm "gitlab.com/phpboyscout/go/config-aws-ssm"
)

awsConfig, _ := config.LoadDefaultConfig(ctx)
client := ssm.NewFromConfig(awsConfig)

store, err := cfg.NewStore(ctx,
	cfg.WithFiles(fsys, "/etc/app.yaml"),                            // YAML defaults
	cfg.WithBackend(configawsssm.FromClient(client, "/app")),        // SSM outranks them
)

An SSM layer takes part in precedence, per-key merge, provenance and hot-reload exactly as a file does. Parameter names under the prefix, split on /, become the nested tree:

/app/server/host   (String)      store.View().GetString("server.host") // "localhost"
/app/server/port   (String)   →  store.View().GetInt("server.port")    // 8080

Provenance names the prefix: store.View().Origin("server.port") renders as aws-ssm:/app/.

Read-only

v0.1.0 is read-only. Parameter Store has no compare-and-swap write — PutParameter either creates-if-absent or clobbers unconditionally, with no "put if version == N" — so the version-at-load conflict trap that a writable backend must honour cannot be honoured for an update. Rather than ship a write with a hidden last-write-wins race, this layer participates as an ordinary read-only source; the Store's routing skips it as a write target. Write support is a tracked follow-on. See the spec for the full reasoning.

Value types

Parameter Store holds strings, so by default every String value is a scalar string and the View's typed accessors coerce it (GetInt parses "8080"). Two types are handled specially:

  • A StringList (comma-separated) becomes a real []string leaf, so GetStringSlice returns the list rather than one joined string.
  • A SecureString is read decrypted (WithDecryption), so its plaintext is usable as configuration. Because the layer then carries a decrypted secret, it declares itself Sensitive, which engages the core's leak guard: a write to a key this backend defines will be refused (ErrSensitiveLeak) rather than routed down into a plain writable file beneath. A prefix that holds no SecureString carries no secret and is not marked sensitive, so ordinary configuration routes normally.

For the style where a String key holds a whole JSON or YAML document, pass a value codec — any config.Codec, which the sibling format adapters implement:

import configjson "gitlab.com/phpboyscout/go/config-json"

configawsssm.FromClient(client, "/app", configawsssm.WithValueCodec(configjson.Codec{}))

A String value that decodes to an object becomes a subtree; a value that is a bare scalar stays a string. StringList and SecureString values are never run through the codec. You inject the one format your store actually holds, so this module takes no codec dependency of its own.

Watching

Parameter Store has no change feed, so watch is polling. The backend implements WatchableBackend, calling GetParametersByPath on an interval and firing a reload when any parameter's version — or the set of names — has moved. The default interval is a conservative 60 seconds (parameter-store configuration changes infrequently and SSM's read API is rate limited), which the backend advertises through config.PollIntervalHinter so a default Store.Watch adopts it in place of the eager 2-second local-file default. WithPollInterval(d) overrides that cadence, an explicit config.WithPollInterval on the Store wins over the hint, and the poll loop backs off on ThrottlingException.

Injecting the client

FromClient wraps the real SDK client. For testing, or to supply your own client wrapper, New takes the narrow ParamStore interface directly and Wrap adapts a *ssm.Client to it — so the whole unit suite runs against a fake, with no AWS account.

What it costs

The config graph plus the AWS SDK for Go v2, pulling only the SSM service — five modules (aws-sdk-go-v2, its two internal endpoint modules, service/ssm and smithy-go), asserted by an allowlist test so an unforeseen transitive addition fails the build rather than arriving quietly. A consumer who also builds the client with config.LoadDefaultConfig pulls a larger credential-loading graph, but that is theirs, not the adapter's. The testcontainers-go / LocalStack integration suite is a test-only dependency and reaches no consumer.

License

See LICENSE.

Documentation

Overview

Package configawsssm contributes configuration from an AWS Systems Manager Parameter Store hierarchy as a first-class config layer: precedence, per-key merge, provenance and safe poll-based hot-reload, exactly like a file layer.

A prefix scopes the backend to one hierarchy of the store; the parameter names beneath it, split on "/", become the layer's nested tree. Parameter Store holds strings, so a value is a scalar string by default and the View's typed accessors coerce it (GetInt("server.port") parses "8080"). A StringList becomes a []string leaf; a SecureString is read decrypted, which makes the whole layer Sensitive so the core's leak guard stops a decrypted secret being written into a plain layer beneath. A String value that is itself a JSON or YAML document is decoded into a subtree when a codec is supplied with WithValueCodec — see the config-aws-ssm spec, D3.

The store is reached through the narrow ParamStore interface, which Wrap adapts from a configured *ssm.Client. Injecting the client keeps every credential, Region, endpoint and KMS-key decision with the consumer and lets the whole unit suite run against a fake, needing no AWS account.

v0.1.0 is read-only: Parameter Store has no compare-and-swap write, so the version-at-Load conflict trap the shared conformance suite enforces cannot be honoured, and read-only is a first-class outcome (spec D4, D9). The backend implements config.Backend and config.WatchableBackend but not config.WritableBackend; the Store's routing skips it as a write target.

Index

Examples

Constants

View Source
const SourceKind = config.SourceKind("aws-ssm")

SourceKind is what a layer from this backend reports as, so provenance can distinguish an SSM value from a file or an environment variable — Origin("server.port") renders as "aws-ssm:/app/".

Variables

View Source
var ErrNoRegion = errors.NewSentinel("configawsssm.no_region",
	"no AWS region in the supplied config; set one, or set AWS_REGION for the ambient rung")

ErrNoRegion reports a configuration that names no AWS region.

A config with no region names a parameter in an account nobody chose, so it is refused here rather than failing at the first request. There is no default: AWS documents none, and inventing one is a different act from adopting a provider's own — which is why go/awsclient refuses too.

Functions

func FromClient

func FromClient(client *ssm.Client, prefix string, opts ...Option) config.Backend

FromClient is the common path: New over a Wrap-ped SSM client, so a consumer writes configawsssm.FromClient(client, "/app") without touching the narrow interface.

Example

ExampleFromClient reads configuration from an AWS SSM Parameter Store hierarchy. You build and configure the SSM client — Region, credentials, endpoint and the KMS key that decrypts a SecureString all stay yours — and hand it in with a prefix that scopes and is stripped from the names, so /app/server/port in Parameter Store reads back as server.port.

package main

import (
	"context"
	"fmt"
	"log"

	awscfg "github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/ssm"
	"gitlab.com/phpboyscout/go/config"

	configawsssm "gitlab.com/phpboyscout/go/config-aws-ssm"
)

func main() {
	ctx := context.Background()

	awsConfig, err := awscfg.LoadDefaultConfig(ctx)
	if err != nil {
		log.Fatal(err)
	}

	client := ssm.NewFromConfig(awsConfig)

	store, err := config.NewStore(ctx,
		config.WithBackend(configawsssm.FromClient(client, "/app")),
	)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(store.View().GetInt("server.port"))
}

func FromConfig added in v0.3.0

func FromConfig(cfg aws.Config, prefix string, opts ...Option) (config.Backend, error)

FromConfig builds the SSM client from an aws.Config the caller resolved — a specific profile, an assumed role, a client pointed at LocalStack — and returns a backend over the prefix.

ssm.NewFromConfig does no I/O, returns no error and needs no close, so this rung constructs eagerly and reaching Parameter Store stays deferred to Load, which has a context to bound it.

Where the ambient rung is

Deliberately NOT here. Resolving the ambient AWS chain costs this module ten further dependencies — sso, ssooidc, sts, imds and the rest — and charging every consumer for a credential graph they may never use would spend the dependency-footprint guarantee this adapter states in its own test.

It lives in the ambient subpackage instead, over go/awsclient, so a consumer who wants it pays for it and a consumer who does not is unaffected:

import ssmambient "gitlab.com/phpboyscout/go/config-aws-ssm/ambient"

b, err := ssmambient.Default("/app")

To share ONE resolved chain across several adapters — and across go/signing and go/encryption — resolve it with go/awsclient and pass the config here (spec 0012 L-4, L-5).

func New

func New(store ParamStore, prefix string, opts ...Option) config.Backend

New returns a backend contributing the Parameter Store hierarchy under prefix as a config layer. store is the injected client — a fake in tests, a Wrap-ped *ssm.Client in production; prefix scopes and is stripped from the names.

Types

type Option

type Option func(*backend)

Option configures a backend.

func WithPollInterval

func WithPollInterval(d time.Duration) Option

WithPollInterval sets the watch poll cadence, overriding the 60-second default (D7). A consumer who needs fresher config and knows their rate budget can shorten it; one watching many prefixes can lengthen it. This cadence is what the backend advertises through config.PollIntervalHinter, so a default Store.Watch adopts it in place of the eager 2-second local-file default; an explicit config.WithPollInterval on the Store still wins, and the resolved interval is honoured by the poll loop either way.

func WithValueCodec

func WithValueCodec(codec config.Codec) Option

WithValueCodec decodes each String value through codec: a value that decodes to a mapping becomes a subtree at its key's path, and a value the codec rejects — a bare scalar, or bytes that are not a document — stays a scalar string, so a prefix mixing flat keys and object blobs reads correctly. Omitted, every value is a scalar string.

codec is any config.Codec — the interface the sibling format adapters implement — so a JSON-blob store is read with configjson.Codec{} and a YAML-blob store with the core's, and this module takes no codec dependency of its own.

The codec is offered only for String values: a StringList is already a list, and a decrypted SecureString is a secret scalar, so decode-or-string stays unambiguous (D3).

type Param

type Param struct {
	// Name is the full SSM parameter name, prefix included (e.g. "/app/server/port").
	Name string
	// Value is the parameter's value: the plain string for a String, the
	// comma-separated members for a StringList, and — because the adapter reads
	// with decryption — the plaintext for a SecureString.
	Type string
	// Value holds the parameter's value (see the field doc above); it follows Type
	// for gofumpt's field-alignment, not for meaning.
	Value string
	// Version is the parameter's monotonic per-name version, the poll loop's exact
	// change marker (D7) — no value diffing needed.
	Version int64
}

Param is one Parameter Store entry, with the version the watch path compares.

type ParamStore

type ParamStore interface {
	// GetByPath returns every parameter under prefix — recursive, paginated and
	// fully drained into one slice. decrypt requests SecureString plaintext (D3,
	// D6). Each [Param] carries the Version the poll loop compares to detect
	// change (D7). An absent prefix returns an empty slice and a nil error, not an
	// error, so the backend can report it not-there.
	GetByPath(ctx context.Context, prefix string, decrypt bool) ([]Param, error)
}

ParamStore is the slice of SSM this adapter uses, behind an interface it owns so a fake drives the unit suite and the real client is adapted by Wrap.

It is read-only in v0.1.0: a single method that both Load and the poll loop use. The write verbs are deliberately absent — they are the deferred write task (spec D9), which will widen this interface when it lands.

func Wrap

func Wrap(client *ssm.Client) ParamStore

Wrap adapts a configured SSM SDK client to the narrow ParamStore interface. The client carries every credential, Region, endpoint and retry decision — and the KMS key that decrypts a SecureString; Wrap adds none — it only translates calls (spec D5, D6).

Directories

Path Synopsis
Package ambient adds the zero-conf rung to config-aws-ssm.
Package ambient adds the zero-conf rung to config-aws-ssm.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL