Reading configuration¶
An application with proper architecture has plugable configuration which is easy to tune according to changing environment, like servers, DNS names, and orchestration strategies.
Most of the time you would use a framework like Django, Flask, or FastAPI as foundation for your infrastructure (implementation details) layer. These frameworks tend to provide configuration instruments like Django settings file, Flask application factory, and so on.
These defaults could serve you needs so far, but in more complex applications which includes a lot of moving parts such approach could hit design limations pretty soon.
dependencies
library draw the border between your infrastructure layer and
your business domain. The way you configure your application is implementation
detail itself.
Principles¶
- Hard-coded configuration
- Reading configuration file
- Reading environment variables
- Reading settings from network
Hard-coded configuration¶
Most easiest and straightforward way to define project settings is to hardcode
it inside application Injector
subclass. Such approach would work nice during
proof of concept development stage. Early in the development days you would
probably more focused on business idea itself rather environment it would run
inside.
>>> from dependencies import Injector
>>> from application import App, PostgreSQL, Redis
>>> class Container1(Injector): # doctest: +SKIP
... app = App
... database = this.Database.connection
... cache = this.Cache.connection
...
... class Database(Injector):
... connection = PostgreSQL
... host = (this << 1).settings["postgresql"]["host"]
... port = (this << 1).settings["postgresql"]["port"]
...
... class Cache(Injector):
... connection = Redis
... host = (this << 1).settings["redis"]["host"]
... port = (this << 1).settings["redis"]["port"]
...
... settings = {
... "postgresql": {
... "host": "localhost",
... "port": 5432,
... },
... "redis": {
... "host": "localhost",
... "port": 6379,
... },
... }
>>> Container1.app.run() # doctest: +SKIP
Connecting to localhost:5432
Connecting to localhost:6379
Reading configuration file¶
For simple deployments configuration file would be an obvious choice. Let say
you have this file somewhere in your /etc
folder on your server.
---
postgresql:
host: 127.0.0.3
port: 5432
redis:
host: 127.0.0.3
port: 6379
We could create a singleton configuration instance. File would be read only once.
>>> from functools import lru_cache
>>> from dependencies import Injector, value
>>> from yaml import safe_load
>>> from application import App, PostgreSQL, Redis
>>> class Container2(Injector): # doctest: +SKIP
... app = App
... database = this.Database.connection
... cache = this.Cache.connection
...
... class Database(Injector):
... connection = PostgreSQL
... host = (this << 1).settings["postgresql"]["host"]
... port = (this << 1).settings["postgresql"]["port"]
...
... class Cache(Injector):
... connection = Redis
... host = (this << 1).settings["redis"]["host"]
... port = (this << 1).settings["redis"]["port"]
...
... @value
... @lru_cache
... def settings(config):
... with open(config) as f:
... return safe_load(f)
...
... config = "docs/config.yml"
>>> Container2.app.run() # doctest: +SKIP
Connecting to 127.0.0.3:5432
Connecting to 127.0.0.3:6379
As you may notice the only thing that changed was settings
definition and its
dependency with path to the file. That make it obvious we could define
application container by reusing our first definition with hardcoded settings.
>>> from functools import lru_cache
>>> from dependencies import value
>>> from yaml import safe_load
>>> class Container3(Container1): # doctest: +SKIP
... @value
... @lru_cache
... def settings(config):
... with open(config) as f:
... return safe_load(f)
...
... config = "docs/config.yml"
>>> Container2.app.run() # doctest: +SKIP
Connecting to 127.0.0.3:5432
Connecting to 127.0.0.3:6379
Reading environment variables¶
Some deployment options makes it hard to create configuration files on the target host system. Heroku advertise its twelve-factor application approach heavily. Sometimes environment variables would be an option. But consider it a security risk.
>>> from os import environ
>>> from dependencies import Injector, value
>>> from application import App, PostgreSQL, Redis
>>> class Container(Injector): # doctest: +SKIP
... app = App
... database = this.Database.connection
... cache = this.Cache.connection
...
... class Database(Injector):
... connection = PostgreSQL
... host = (this << 1).settings["postgresql"]["host"]
... port = (this << 1).settings["postgresql"]["port"]
...
... class Cache(Injector):
... connection = Redis
... host = (this << 1).settings["redis"]["host"]
... port = (this << 1).settings["redis"]["port"]
...
... @value
... def settings():
... return {
... "postgresql": {
... "host": environ["POSTGRESQL_HOST"],
... "port": environ["POSTGRESQL_PORT"],
... },
... "redis": {
... "host": environ["REDIS_HOST"],
... "port": environ["REDIS_PORT"],
... },
... }
>>> Container.app.run() # doctest: +SKIP
Connecting to postgresql-instance1.cg034hpkmmjt.us-east-1.rds.amazonaws.com:5432
Connecting to redis-01.7abc2d.0001.usw2.cache.amazonaws.com:6379
Reading settings from network¶
A best option for solid deployment setup would be some variant of service discovery platform.
You could conside combination of HashiCorp Consul and Vault the editors choice.
We discover database and cache host using consul client. After that we request a password for found service from vault. Optionaly, we could configure consul client using environment variables. Let say we could change default port for local consul agent.
>>> from functools import lru_cache
>>> from consul import Consul
>>> from dependencies import Injector
>>> from application import App, PostgreSQL, Redis
>>> class Container(Injector): # doctest: +SKIP
... app = App
... database = this.Database.connection
... cache = this.Cache.connection
...
... class Database(Injector):
... connection = PostgreSQL
... host = (this << 1).settings["postgresql"]["host"]
... port = (this << 1).settings["postgresql"]["port"]
...
... class Cache(Injector):
... connection = Redis
... host = (this << 1).settings["redis"]["host"]
... port = (this << 1).settings["redis"]["port"]
...
... @value
... @lru_cache
... def settings(consul):
... return {
... "postgresql": {
... "host": consul.kv.get("postgresql_host")[1]["Value"],
... "port": consul.kv.get("postgresql_port")[1]["Value"],
... },
... "redis": {
... "host": consul.kv.get("redis_host")[1]["Value"],
... "port": consul.kv.get("redis_port")[1]["Value"],
... },
... }
...
... consul = Consul
>>> Container.app.run() # doctest: +SKIP
Connecting to local consul agent
Connecting to postgresql-instance1.cg034hpkmmjt.us-east-1.rds.amazonaws.com:5432
Connecting to redis-01.7abc2d.0001.usw2.cache.amazonaws.com:6379
— ⭐ —