Skip to content

Read and write Azure App Configuration

Azure App Configuration becomes a config layer through the sibling module config-azure-appconfig, taken only by consumers who need it.

go get gitlab.com/phpboyscout/go/config-azure-appconfig

You build and configure the App Configuration client — endpoint and credential (an azidentity token or a connection string) stay yours — and hand it in with a key prefix:

import (
    "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
    "github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig"
    "gitlab.com/phpboyscout/go/config"
    configazure "gitlab.com/phpboyscout/go/config-azure-appconfig"
)

cred, _ := azidentity.NewDefaultAzureCredential(nil)
client, _ := azappconfig.NewClient(endpoint, cred, nil)

store, err := config.NewStore(ctx,
    config.WithFiles(fsys, "/etc/app.yaml"),
    config.WithBackend(configazure.FromClient(client, "app/")),   // outranks the file
)

Keys under the prefix nest into the tree, and the layer takes part in precedence, per-key merge, provenance and hot-reload like a file. Values are scalar strings by default; pass a config.Codec with WithValueCodec to decode a document-valued setting. The declared content-type is not used to pick a codec — decoding is always the one you inject.

When you do not need this

If the values are static, or already arrive as environment variables, WithEnv covers them with no SDK.

Reach for App Configuration when values are managed centrally and change without a redeploy — and for its labels, which let one store serve several environments from the same keys.

Labels

App Configuration keys are identified by (key, label). Scope a backend to one label with WithLabel; omitted, it reads the no-label set:

configazure.FromClient(client, "app/", configazure.WithLabel("prod"))

The label scopes reads and pins writes; provenance names it (azure-appconfig:app/@prod). Feature-flag settings are filtered out (they are a distinct concern), and a Key Vault reference is left as an opaque string rather than resolved — resolving secrets is Key Vault's own adapter.

Writing

config-azure-appconfig is read and write. A write is a per-key compare-and-swap on the setting's ETag captured at load: if the setting moved since, the write is refused with config.ErrConflict rather than clobbering it. A multi-key batch is applied per key (App Configuration has no transaction, so AtomicMultiKey is false); a mid-batch conflict rolls the already-written keys back, best effort.

Watching

The adapter joins hot-reload by polling (NativeWatch: false). The efficient default is a conditional GET on a sentinel key (WithSentinelKey) — one cheap request that 304s until the sentinel's ETag changes — falling back to a full re-list. WithPollInterval sets the cadence.

Getting a client

Building the client yourself is the default. Rung 3 has two shapes here, because App Configuration genuinely does:

// A principal, with the endpoint supplied separately.
b, err := configazureappconfig.FromCredential(cred, endpoint, "app/")

// A connection string, which carries the endpoint AND the secret together.
b, err := configazureappconfig.FromConnectionString(conn, "app/")

// Nothing but the endpoint — note the separate import.
import acambient "gitlab.com/phpboyscout/go/config-azure-appconfig/ambient"

b, err := acambient.Default(ctx, endpoint, "app/")

Neither rung-3 shape is a special case of the other, so both are rungs.

The connection string is a secret — it embeds an access key. This adapter never logs it, and reports a malformed one without echoing the input, because that is how a secret reaches a log. Treat it as you would a password.

The endpoint is always required, and a nil credential is refused including a typed nil.

What it costs

Modules added 14 — 5 for the App Configuration SDK, 9 for the config graph

The config graph plus the App Configuration SDK (azappconfig, azcore and two support modules — five, asserted by an allowlist test). azidentity is yours, built with the client, not the adapter's.