Cloud Configuration Providers
Cloud configuration providers fetch configuration from remote sources, enabling centralized configuration management, dynamic updates, and multi-environment support.
Available Providers
| Provider | Module | Platform | Use Case |
|---|---|---|---|
| REST Client | config-rest-client | Multiplatform | Generic REST config servers |
| Azure App Configuration | azure-app-config | JVM | Azure cloud deployments |
CloudConfigProvider Interface
All cloud providers implement the CloudConfigProvider interface:
interface CloudConfigProvider : PropertySource<Map<String, Any>> {
val providerId: String
val isAvailable: Boolean
val configLevel: ConfigLevel
val lastRefreshedAt: Instant?
suspend fun refresh(): IdkResult<RefreshResult, IdkError>
fun watchForChanges(): Flow<ConfigChangeEvent>
suspend fun startWatching()
suspend fun stopWatching()
suspend fun healthCheck(): IdkResult<CloudProviderHealth, IdkError>
}
Data Classes
data class RefreshResult(
val configCount: Int, // Number of configuration keys loaded
val changedKeys: Set<String>, // Keys that changed since last refresh
val refreshedAt: Instant // Timestamp of refresh
)
data class ConfigChangeEvent(
val changedKeys: Set<String>,
val source: String,
val timestamp: Instant
)
data class CloudProviderHealth(
val isHealthy: Boolean,
val latencyMs: Long,
val configKeyCount: Int,
val lastSuccessfulRefresh: Instant?,
val errorMessage: String?
)
REST Configuration Client
The config-rest-client module provides a multiplatform REST API client for fetching configuration from remote servers.
Installation
// build.gradle.kts
dependencies {
implementation(project(":lib-conf-config-rest-client"))
implementation(project(":lib-conf-config-offline-cache")) // Optional
}
Configuration
data class RestConfigProviderConfig(
val baseUrl: String,
val tenantId: String,
val settingsPath: String = "/api/v1/config/settings",
val refreshPath: String = "/api/v1/config/refresh",
val auth: RestAuthConfig? = null,
val connectTimeoutMs: Long = 5000,
val readTimeoutMs: Long = 30000,
val refreshIntervalMs: Long = 60000, // 1 minute
val offlineCacheEnabled: Boolean = true,
val baseConfig: CloudConfigProviderConfig = CloudConfigProviderConfig()
)
Authentication Options
- Bearer Token
- Basic Auth
- API Key
- OAuth 2.0
val config = RestConfigProviderConfig(
baseUrl = "https://config.mycompany.com",
tenantId = "my-tenant",
auth = RestAuthConfig.BearerToken("my-jwt-token")
)
val config = RestConfigProviderConfig(
baseUrl = "https://config.mycompany.com",
tenantId = "my-tenant",
auth = RestAuthConfig.BasicAuth(
username = "config-user",
password = "secret"
)
)
val config = RestConfigProviderConfig(
baseUrl = "https://config.mycompany.com",
tenantId = "my-tenant",
auth = RestAuthConfig.ApiKey(
headerName = "X-API-Key",
apiKey = "my-api-key"
)
)
val config = RestConfigProviderConfig(
baseUrl = "https://config.mycompany.com",
tenantId = "my-tenant",
auth = RestAuthConfig.OAuth2(
tokenUrl = "https://auth.mycompany.com/oauth2/token",
clientId = "config-client",
clientSecret = "client-secret",
scopes = listOf("config:read")
)
)
Basic Usage
val provider = RestConfigProviderImpl(
config = config,
httpClient = HttpClient(CIO),
offlineCache = KmpSettingsStorage()
)
// Initial load
val refreshResult = provider.refresh()
if (refreshResult.isOk) {
println("Loaded ${refreshResult.value.configCount} configuration keys")
}
// Access properties
val dbHost = provider.getProperty("database.host", String::class)
val timeout = provider.getProperty("http.timeout", Int::class, 5000)
API Compatibility
The REST client expects these endpoints:
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/config/settings | List settings |
| GET | /api/v1/config/settings/{key} | Get setting by key |
| POST | /api/v1/config/refresh | Trigger cache refresh |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
scope | String | APP, TENANT, or PRINCIPAL |
profile | String | Configuration profile |
keyPrefix | String | Filter by key prefix |
Azure App Configuration
The azure-app-config module provides integration with Azure App Configuration service.
Installation
// build.gradle.kts
dependencies {
implementation(project(":lib-conf-azure-app-config"))
// Azure SDK
implementation(platform("com.azure:azure-sdk-bom:1.2.18"))
implementation("com.azure:azure-data-appconfiguration")
implementation("com.azure:azure-identity")
}
Configuration
data class AzureAppConfigProviderConfig(
val connectionString: String? = null,
val endpoint: String? = null,
val credentialOptions: AzureCredentialOptions? = null,
val keyFilter: String = "*",
val labelFilter: String? = null,
val refreshInterval: Duration = 30.seconds,
val sentinelKey: String? = null,
val keyPrefix: String? = null,
val trimKeyPrefix: Boolean = true,
val offlineCacheEnabled: Boolean = true,
val tenantId: String? = null,
val baseConfig: CloudConfigProviderConfig = CloudConfigProviderConfig()
)
Authentication Methods
- Default Credential (Recommended)
- Connection String
- Managed Identity
- Service Principal
// Recommended for production - uses DefaultAzureCredential
val config = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
credentialOptions = AzureCredentialOptions(
authMethod = AzureAuthMethod.DEFAULT_CREDENTIAL
),
labelFilter = "production"
)
// Simple but less secure
val config = AzureAppConfigProviderConfig(
connectionString = "Endpoint=https://myapp.azconfig.io;Id=...;Secret=..."
)
// System-assigned managed identity
val config = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
credentialOptions = AzureCredentialOptions(
authMethod = AzureAuthMethod.MANAGED_IDENTITY
)
)
// User-assigned managed identity
val config = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
credentialOptions = AzureCredentialOptions(
authMethod = AzureAuthMethod.USER_ASSIGNED_MANAGED_IDENTITY,
managedIdentityClientId = "12345678-1234-1234-1234-123456789012"
)
)
val config = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
credentialOptions = AzureCredentialOptions(
authMethod = AzureAuthMethod.CLIENT_SECRET,
tenantId = "your-tenant-id",
clientId = "your-client-id",
clientSecret = "your-client-secret"
)
)
Label-Based Environment Separation
Azure App Configuration uses labels to separate environments:
// Production environment
val prodConfig = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
labelFilter = "production"
)
// Staging environment
val stagingConfig = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
labelFilter = "staging"
)
// No label (default values)
val defaultConfig = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
labelFilter = null // or "\\0" for no label
)
Key Conventions
Keys are parsed to determine their scope:
| Key Format | Scope | Example |
|---|---|---|
{key} | APP | database.host |
tenant.{tenantId}.{key} | TENANT | tenant.acme.feature.enabled |
principal.{tenantId}.{principalId}.{key} | PRINCIPAL | principal.acme.alice.theme |
Change Watching
Both providers support watching for configuration changes.
Polling-Based Watching
// Start watching (uses configured refresh interval)
provider.startWatching()
// Collect change events
provider.watchForChanges().collect { event ->
println("Config changed: ${event.changedKeys}")
println("Source: ${event.source}")
println("Time: ${event.timestamp}")
// React to specific changes
if ("feature.maintenance-mode" in event.changedKeys) {
enableMaintenanceMode()
}
}
// Stop watching
provider.stopWatching()
Sentinel Key (Azure)
Azure supports sentinel keys for efficient change detection:
val config = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
sentinelKey = "settings:sentinel", // Only poll this key
refreshInterval = 30.seconds
)
// When you update configuration in Azure:
// 1. Update your config values
// 2. Update the sentinel key value
// 3. Client detects sentinel change and refreshes all config
Health Checks
Monitor provider health for observability:
val health = provider.healthCheck()
if (health.isOk) {
val status = health.value
println("Healthy: ${status.isHealthy}")
println("Latency: ${status.latencyMs}ms")
println("Keys: ${status.configKeyCount}")
println("Last refresh: ${status.lastSuccessfulRefresh}")
} else {
println("Health check failed: ${health.error.message}")
}
// Integration with health endpoint
@Inject
class HealthController(
private val configProvider: CloudConfigProvider
) {
suspend fun checkHealth(): HealthResponse {
val configHealth = configProvider.healthCheck()
return HealthResponse(
config = if (configHealth.isOk) "UP" else "DOWN",
configLatency = configHealth.getOrNull()?.latencyMs
)
}
}
Offline Fallback
Cloud providers can use offline cache for network failure resilience.
Configuration
val provider = RestConfigProviderImpl(
config = RestConfigProviderConfig(
baseUrl = "https://config.mycompany.com",
tenantId = "my-tenant",
offlineCacheEnabled = true
),
offlineCache = KmpSettingsStorage()
)
Fallback Behavior
Best Practices
1. Use DefaultAzureCredential in Azure
// Recommended - flexible credential chain
val config = AzureAppConfigProviderConfig(
endpoint = "https://myapp.azconfig.io",
credentialOptions = AzureCredentialOptions(
authMethod = AzureAuthMethod.DEFAULT_CREDENTIAL
)
)
2. Always Enable Offline Cache
// Resilient to network failures
val provider = RestConfigProviderImpl(
config = config.copy(offlineCacheEnabled = true),
offlineCache = KmpSettingsStorage()
)
3. Use Labels for Environments
val config = AzureAppConfigProviderConfig(
labelFilter = when (environment) {
"prod" -> "production"
"staging" -> "staging"
else -> "development"
}
)
4. Handle Refresh Failures Gracefully
suspend fun refreshWithRetry(maxRetries: Int = 3): RefreshResult? {
repeat(maxRetries) { attempt ->
val result = provider.refresh()
if (result.isOk) {
return result.value
}
log.warn("Refresh attempt ${attempt + 1} failed: ${result.error.message}")
delay(1000L * (attempt + 1)) // Exponential backoff
}
log.error("All refresh attempts failed, using cached config")
return null
}
5. Use Sentinel Keys for Efficient Polling
// Only polls sentinel key, not all configuration
val config = AzureAppConfigProviderConfig(
sentinelKey = "app:version",
refreshInterval = 30.seconds
)