- Azure SDK supports global configuration through Common environment variables
- .NET allows to configure SDKs through
appsettings.jsonor any otherMS.Extensions.Configuration-compatible source using additionalMicrosoft.Extensions.Azurepackage. - Java Azure Spring Cloud works on extensive list of configuration options
- Cloud Configuration draft
- Define a way to apply non-code configuration to Azure SDK
- support idiomatic configuration approaches for different frameworks/languages
- Define guidelines for SDKs and languages to follow
- property conventions: structure, naming principles
- configuration API and extensibility points
- ambient vs explicit configuration and priority
- error handling and debugging
The main consumers for this feature are web-framework integrations with Azure SDKS such as Azure Spring Cloud (or any future integrations with frameworks and environments).
- Make explicit configuration fully-consistent across languages
- Have great experience for mixed code and non-code configuration: it must still work reasonably and deterministically, but we're not optimizing for this case
- Define all configuration properties in every language. Defining properties can be done for options/clients gradually as a next steps
Configuration should support arbitrary format: json/yml/xml, flat. The only common denominator is getting property by name (+ metadata). Conventions on naming are different between languages/frameworks: separators, casing, etc.
-
.NET: already supports configuration with with MS.Extensions.Configuration, no changes suggested, provided to demonstrate variety of needs
{ "AzureDefaults": { "Retry": { "MaxRetries": 3, "Mode": "Exponential", "Delay": "00:00:01", "MaxDelay": "00:00:30" } }, "KeyVault": { "VaultUri": "https://mykeyvault.vault.azure.net" }, "Storage": { "ServiceUri": "https://mydemoaccount.storage.windows.net" } }services.AddAzureClients(builder => { builder.AddBlobServiceClient(Configuration.GetSection("Storage")); builder.ConfigureDefaults(Configuration.GetSection("AzureDefaults")); }
Azure Core in different languages have different abstractions for retry options, so configuration properties might look differently:
-
Java: new, similar approach already exists in Azure Spring integrations, Quarkus and Micronaut have similar configuration stories
http.retry.strategy = exponential http.retry.strategy.exponential.max-retries = 7 http.retry.strategy.exponential.base-delay = PT1S http.retry.strategy.exponential.max-delay = PT2S appconfiguration.http.proxy.host = appconfigproxy.contoso.com appconfiguration.http.proxy.port = 80 appconfiguration.connection-string = DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=mykey storage.connection-string = DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=mykey
or (Azure Spring configuration)
spring: cloud: azure: storage: fileshare: account-name: myaccount account-key: mykey endpoint: https://endpoint.com
// Configuration source is new; we don't need to ship FileSource implementation in Core, abstraction is a good start com.azure.core.util.Configuration configuration = new Configuration(new org.example.FileSource("application.properties")); // API exists today, but supports a few common configuration properties new BlobServiceClientBuilder() .configuration(configuration) .build();
-
Define
Configurationabstraction in azure core - it's an interface for clients to retrieve any kinds of properties- can retrieve property by name (and optional metadata)
- support pluggable sources
- support ambient (env vars) and explicit configuration
- .NET is special here, it only needs to work with
MS.Extensions.Configuration
-
Require clients and core options to read themselves from
Configuration.- Each client or core option defines configuration properties for itself and owns reading itself from
Configuration
- Each client or core option defines configuration properties for itself and owns reading itself from
-
Configuration properties names and hierarchy follow public APIs in given client/language
- They evolve with public APIs, have breaking changes expectations similar to public APIs
- They should be documented similarly to public APIs (or point to the same documentation)
-
Client config properties are local (specific to client). Core/common properties can be local (per-client) or global (per all clients).
-
Allow applications and framework integrations to implement custom sources and explicitly add them to
Configuration. Web frameworks have opinions on and support for- source: file, env vars, etc
- file format: yml, json, xml, flat
- conventions on naming: separators, casing, binding to property names in code
-
Supporting popular frameworks and matching existing public APIs for given client/language for custom sources is more important than consistency across languages
- Properties reasonably follow public API (naming and hierarchy)
- When possible, properties should be consistent across languages
- Defined environment variables must be consistent across languages (frameworks may support it in some other way - beyond our control)
Property uniquely identifies specific public property/setter/constructor-parameter that configures option or client.
-
ConfigurationSourcecan only retrieve property value by given name- MUST: get property value by name
- MUST: return string values only (to enable all possible sources including env vars), parsing/conversion happen between options and
Configuration - MUST: consistently return the same value for the same property name (e.g. if duplicate keys are present))
- MUST NOT: change values
- SHOULD NOT: remap property name segments
- MAY: add prefixes to property names, map separators and casing for names, translate flat property name to path in file
-
Client should be able to request a property from
Configurationby string name- MUST: get value for given name
- MUST: allow checking if value for given name exists OR allow retrieving with default value
- SHOULD: allow requesting value with given type
- MAY: retrieve per-client property and fallback to global configuration when applicable
- MAY: get value for given list of property name aliases (for backward compatibility with existing properties if any)
-
Property name MUST map unambiguously to corresponding property/setter/constructor parameter/etc in public API
- MUST: include implementation class identifier or other means to instantiate specific options class
- MUST: have the same name for the same API property (potentially prefixed) across clients and other options they are used in.
- SHOULD: use separators, casing and conventions idiomatic for language
- SHOULD: use annotations/attributes to describe, document and validate property names + metadata along with the code.
-
Environment variable names, when defined, MUST be consistent across languages:
ConfigurationMUST only support retrieving known environment variables- new environment variables MAY be registered for new properties
- new env vars names MUST keep following current conventions
-
Configuration instance is immutable. Client options can read configuration when it's passed or do it lazily - it leads to the same resolved values
- Single
ConfigurationSource(orConfiguration) instance MUST consistently return the same value for the same property name- Log level needs special treatment - it MAY dynamically change (not in scope of this spec)
- There might be a few other dynamic properties in future
ConfigurationMUST NOT allow adding new sources or properties after it's initialized.- Caveat:
Configurationexists in some languages and need more work to satisfy this requirement
- Caveat:
- Single
-
All property names MUST be documented along with their metadata, expected format and public API they represent.
Property metadata which MAY be passed to Configuration in addition to property name:
- client name (prefix)
- can be global (
Configurationshould fallback to global properties when property not available for given client) - default values
- property name aliases (for backward compatibility)
Behavior is similar to what code configuration does already, with more edge cases related to missing/invalid/conflicting properties and additional logic in Configuration to retrieve/fallback and cache.
- Every retrieved property value MUST be validated: parsing, type, value range.
- it's a client/option responsibility, but core may provide common helpers
- invalid property value, even if optional, MUST raise an error and fail corresponding client/option setup
- Parsing errors, missing required properties, conflicts MUST be accompanied with informative error log and configuration MUST fail with exception.
- Configuration SHOULD log where it retrieved property from (name and source) with debug severity
- Recoverable conflicts MUST be accompanied with warning log, best effort should be done to detect such cases. Example: attempt to override configuration with higher source priority.
Priority: code > explicit > implicit
-
Implicit configuration (env vars) MUST be applied first before other configurations. It MUST be the last source to retrieve properties from.
-
Configuration with custom sources MUST be passed explicitly during client setup and can override implicit configuration
- Clients/core options MUST expose API to set configuration
- Options SHOULD support creating themselves from configuration (rather than exposing a setter for configuration) - this makes mixed in-code and non-code configuration more predictable
- If requested options are not defined in configuration, caller MUST detect it and MUST keep previous value of such option when it was set.
- Clients/core options MUST expose API to set configuration
-
Configuration properties provided through explicit
Configurationcan be changed in code. -
When option/property was already set up in code, it MUST NOT be overridden with non-code configuration.
BlobServiceClientBuilder()
.httpClient(httpClient)
// must not change httpClient even if fileConfig has relevant properties,
// should result in warning, but proceed
.configuration(fileConfig)
.build();BlobServiceClientBuilder()
.configuration(fileConfig)
// has higher priority than options in fileConfig
.httpClient(httpClient)
.build();APIView (Java): https://apiview.dev/Assemblies/Review/2a7d94c92389414da9448d6aa4c31b4f/4793132eab8749429479997d8501ac02?diffRevisionId=bb3ed257c464403bba88ce61e40bf068&doc=False&diffOnly=True It's a prototype with bare minimum APIs, polishing and additions are expected.
┌────────────────┐
│ ClientBuilder/ │
│ Options ├───get(prop)/getSection(client)──┐
└────────────────┘ │ ┌──────────────────────┐
│ ┌──► Environment variables│
│ │ └──────────────────────┘
┌─────────▼─────┐ │ ┌──────────────────────┐
│ Configuration │──get(prop)─┼──► System properties │
└───────────────┘ │ └──────────────────────┘
│ ┌──────────────────────┐
└──► Custom source │
│ (e.g. Spring yaml) │
└──────────────────────┘
In this example property names are new - they only exist in prototype
-
Code based:
ClientOptions clientOptions = new ClientOptions(); clientOptions.setApplicationId("appconfig-application-id"); List<Header> headers = new ArrayList<>(); headers.add(new Header("header1", "v1")); headers.add(new Header("header2", "v2,v3")); clientOptions.setHeaders(headers); HttpLogOptions httpLogOptions = new HttpLogOptions(); httpLogOptions.setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS); httpLogOptions.setAllowedHeaderNames(Set.of("header1", "header2")); httpLogOptions.setAllowedQueryParamNames(Set.of("p1", "p2")); httpLogOptions.setPrettyPrintBody(true); HttpClientOptions httpClientOptions = new HttpClientOptions(); ProxyOptions proxyOptions = new ProxyOptions(ProxyOptions.Type.HTTP, InetSocketAddress.createUnresolved("localhost", 80)); httpClientOptions.setProxyOptions(proxyOptions); HttpClient httpClient = HttpClient.createDefault(httpClientOptions); // most complicated part: retry count is custom, delays are default, but users have to specify them HttpPipelinePolicy retryPolicy = new RetryPolicy(new ExponentialBackoff(25, Duration.ofMillis(800), Duration.ofSeconds(8)), null, null); ConfigurationClientBuilder builder = new ConfigurationClientBuilder() .httpLogOptions(httpLogOptions) .clientOptions(clientOptions) .httpClient(httpClient) .retryPolicy(retryPolicy) .serviceVersion(ConfigurationServiceVersion.V1_0) .connectionString("...");
-
Proposed property-based config
http-client.proxy.host=localhost http-client.proxy.port=80 http-client.application-id=http-app-id http-client.headers=header1=v1;header2=v2,v3 http.logging.level=BODY_AND_HEADERS http.logging.allowed-header-names=header3,header4 http.logging.allowed-query-param-names=p1,p2 http.logging.pretty-print-body=true http-client.retry.max-retries=5 appconfiguration.service-version=V1_0 appconfiguration.connection-string=... appconfiguration.http-client.application-id=appconfig-app-id
Configuration configuration = new Configuration(new FileSource("application.properties")); return new com.azure.data.appconfiguration.ConfigurationClientBuilder().configuration(configuration);
Several scenarios to consider: