- 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);
Re: bulk code-configure. I'm a bit concerned about pushing internal usage towards reflective access. That might be a quick short-term solution to the problem, but it comes with all the problems of reflective access, i.e. we lose compiler support here. If we do that, we should plan for cleaning that debt.