Dynamic Configuration

A Service has its configuration which includes the information about its upstream as well as the application-layer specific configurations (e.g. HTTP-based configurations such as manipulating headers and injecting the upstream credentials) for various widely used L-7 protocols and supported by Octelium (read more about Service modes here). Here is an example:

1
kind: Service
2
metadata:
3
name: svc1
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
auth:
11
bearer:
12
fromSecret: apikey1

The above configuration is the default configuration which is used globally for all the Service requests by default. However, in many cases, you might want to make this configuration dynamic depending on the request's context. For instance, you might want to route to a certain upstream depending on the identity of the User or the L-7 specific context (e.g. HTTP paths, headers, request body, etc...). For such cases, Octelium provides you a very flexible way to apply dynamic configuration where you can define multiple configurations and choose one of them on a per-request basis via rules that are defined exactly the same way as Policy rules (read more about Policies and access control here).

Here is an example where we can provide smart routing based on the User identity to route to additional upstreams. We now have 2 additional configs (i.e. config-1 and config-2) to the default configuration, each having a different credential representing maybe a different account and/or privileges for the upstream. If no rules match or if there are no rules, then the default config is used as a fallback config.

1
kind: Service
2
metadata:
3
name: svc1
4
spec:
5
mode: HTTP
6
# This is our typical static configuration used by default if no rules match
7
config:
8
upstream:
9
url: https://api.example.com
10
http:
11
auth:
12
bearer:
13
fromSecret: apikey0
14
dynamicConfig:
15
# we define a list of named configs that could override the global config
16
configs:
17
- name: config-1
18
upstream:
19
url: https://api-01.example.com
20
http:
21
auth:
22
bearer:
23
fromSecret: apikey1
24
- name: config-2
25
upstream:
26
url: https://api-02.example.com
27
http:
28
auth:
29
bearer:
30
fromSecret: apikey2
31
rules:
32
# Use the Config `config-1` for Users belonging to the Group `group-1`
33
- condition:
34
match: '"group-1" in ctx.user.spec.groups'
35
configName: config-1
36
# Use the Config `config-2` for Users belonging to the Group `group-2`
37
- condition:
38
match: '"group-2" in ctx.user.spec.groups'
39
configName: config-2

As shown in the example above, the dynamicConfig has 2 lists: one for the named dynamic configs represented by the configs list and another, rules, for the rules that are checked upon each request to choose one of these configs when matched. Each rule has a condition that can be defined in the exact same way as Policy rule conditions (read more here). A rule matches if its condition is met and in such case its corresponding named config, represented by the configName field, is chosen to be the configuration for that particular request overriding the default Service configuration.

Dynamic configuration is not restricted to a certain Service mode. The above example can be generalized to any other mode where you can override the default configuration based on whatever dynamic rules you wish to define. For example, you can use it for the POSTGRES and MYSQL modes to route to specific upstreams and/or use specific usernames and passwords as well as databases depending on the identity and/or context of the request. For the case of SSH, you can route to different usernames and passwords/private keys depending on the identity or the request's User. You can explore more mode-specific examples here:

Also, you are not really required to define a default configuration when you have one or more named configurations unless with working with certain situations (e.g. buffering request body in HTTP-based Services (read more here)). Here is an example of a Service that acts as an API gateway:

1
kind: Service
2
metadata:
3
name: my-api
4
spec:
5
mode: HTTP
6
dynamicConfig:
7
configs:
8
- name: v1
9
upstream:
10
url: https://apiv1.example.com
11
http:
12
auth:
13
bearer:
14
fromSecret: apikey1
15
path:
16
removePrefix: /v1
17
- name: v2
18
upstream:
19
url: https://apiv2.example.com
20
http:
21
auth:
22
bearer:
23
fromSecret: apikey2
24
path:
25
removePrefix: /v2
26
rules:
27
# Use the Config `config-1` for Users belonging to the Group `group-1`
28
- condition:
29
match: ctx.request.http.path.startsWith("/v1")
30
configName: v1
31
# Use the Config `config-2` for Users belonging to the Group `group-2`
32
- condition:
33
match: ctx.request.http.path.startsWith("/v2")
34
configName: v2
35
# Fallback to v1 when nothing matches
36
- condition:
37
matchAny: true
38
configName: v1

However, in such case, you need to be careful that your rules cover all the possible conditions.

Inheritance

In some cases, you might have multiple configurations that share one or more fields and sub-fields making the process of writing dynamic configurations mundane and even error-prone for more complex configurations. For example, here is an example for a POSTGRES Service (read more here) where you force the User to use a certain database based on the identity while keeping everything else in the config the same (i.e. same URL, same user and password). Therefore, without Inheritance, your Service looks as follows:

1
kind: Service
2
metadata:
3
name: postgres
4
spec:
5
mode: POSTGRES
6
dynamicConfig:
7
configs:
8
- name: db1
9
upstream:
10
url: postgres://address-to-pg
11
postgres:
12
user: postgres
13
database: db1
14
auth:
15
password:
16
fromSecret: pg-password
17
- name: db2
18
upstream:
19
url: postgres://address-to-pg
20
postgres:
21
user: postgres
22
database: db2
23
auth:
24
password:
25
fromSecret: pg-password
26
rules:
27
- condition:
28
match: '"prod" in ctx.user.spec.groups'
29
configName: db2
30
- condition:
31
matchAny: true
32
configName: db1

Now with Service config inheritance, your Service can become much shorter and easier to maintain and read as follows:

1
kind: Service
2
metadata:
3
name: postgres
4
spec:
5
mode: POSTGRES
6
dynamicConfig:
7
configs:
8
- name: db1
9
upstream:
10
url: postgres://address-to-pg
11
postgres:
12
user: postgres
13
database: my-db1
14
auth:
15
password:
16
fromSecret: pg-password
17
- name: db2
18
parent: db1
19
postgres:
20
# We're overriding the `my-db1` database with `my-db2`
21
# while inheriting everything else from `db1` config
22
database: db2
23
rules:
24
- condition:
25
match: '"prod" in ctx.user.spec.groups'
26
configName: db1
27
- condition:
28
matchAny: true
29
configName: db2

You can use the default configuration whose name is reserved as default as a parent for your dynamic configs. Here is an example:

1
kind: Service
2
metadata:
3
name: postgres
4
spec:
5
mode: POSTGRES
6
config:
7
name: db1
8
upstream:
9
url: postgres://address-to-pg
10
postgres:
11
user: postgres
12
database: my-db1
13
auth:
14
password:
15
fromSecret: pg-password
16
dynamicConfig:
17
configs:
18
- name: db1
19
parent: default
20
postgres:
21
database: db1
22
- name: db2
23
parent: default
24
postgres:
25
database: db2

As shown above, you actually use the default as well as any other dynamic config as templates to be used as parents for other child configs. Service configuration inheritance can help you to actually create very complex dynamic configuration setups without causing degradation of maintainability and readability at scale. Note that Octelium currently only supports one-level inheritance. In other words, you cannot currently have configs whose parent also inherits from another config. Also note that the default config cannot have parents.

© 2025 octelium.comOctelium Labs, LLCAll rights reserved
Octelium and Octelium logo are trademarks of Octelium Labs, LLC.
WireGuard is a registered trademark of Jason A. Donenfeld