nginx_k8s

Nginx sidecar container management abstractions.

The nginx_k8s charm library provides:

  • Nginx: A class to manage a nginx sidecar container.

    Includes regular nginx config file generation, tls configuration, and reload logic.

  • NginxPrometheusExporter: A class to manage a nginx-prometheus-exporter

    sidecar container.

  • NginxConfig: A nginx config file generation wrapper.

class Nginx(
container: Container,
update_ca_certificates_on_restart: bool = True,
liveness_check_endpoint_getter: Callable[[bool], str] | None = None,
)

Bases: object

Helper class to manage the nginx workload.

NGINX_DIR = '/etc/nginx'
NGINX_CONFIG = '/etc/nginx/nginx.conf'
KEY_PATH = '/etc/nginx/certs/server.key'
CERT_PATH = '/etc/nginx/certs/server.cert'
CA_CERT_PATH = '/usr/local/share/ca-certificates/ca.crt'
reconcile(
nginx_config: str,
tls_config: TLSConfig | None = None,
)

Configure pebble layer and restart if necessary.

property are_certificates_on_disk: bool

Return True if the certificates files are on disk.

class NginxConfig(
server_name: str,
upstream_configs: list[NginxUpstream],
server_ports_to_locations: dict[int, list[NginxLocationConfig]],
map_configs: Sequence[NginxMapConfig] | None = None,
enable_health_check: bool = False,
enable_status_page: bool = False,
supported_tls_versions: list[str] | None = None,
ssl_ciphers: list[str] | None = None,
worker_processes: int = 5,
worker_connections: int = 4096,
proxy_read_timeout: int = 300,
proxy_connect_timeout: str = '5s',
)

Bases: object

NginxConfig.

To generate an Nginx configuration for a charm, instantiate the NginxConfig class with the

required inputs:

  1. server_name: The name of the server (e.g. charm fqdn), which is used to identify the server in Nginx configurations.

  2. upstream_configs: List of NginxUpstream used to generate Nginx upstream directives.

  3. server_ports_to_locations: Mapping from server ports to a list of NginxLocationConfig.

Any charm can instantiate NginxConfig to generate an Nginx configuration as follows:

Example::
>>> # illustrative purposes only
>>> import socket
>>> from ops import CharmBase
>>> from charmlibs.nginx_k8s import NginxConfig, NginxUpstream, NginxLocationConfig
...     #[...]
>>> class AnyCharm(CharmBase):
>>>     def __init__(self, *args):
>>>         super().__init__(*args)
...          #[...]
>>>         self._container = self.unit.get_container("nginx")
>>>         self._nginx = NginxConfig(
>>>             server_name=self.hostname,
>>>             upstream_configs=self._nginx_upstreams(),
>>>             server_ports_to_locations=self._server_ports_to_locations(),
>>>         )
...         #[...]
>>>         self._reconcile()
...     #[...]
>>>     @property
>>>     def hostname(self) -> str:
>>>         return socket.getfqdn()
...
>>>     @property
>>>     def _nginx_locations(self) -> List[NginxLocationConfig]:
>>>         return [
>>>             NginxLocationConfig(path="/api/v1", backend="upstream1",modifier="~"),
>>>             NginxLocationConfig(path="/status", backend="upstream2",modifier="="),
>>>         ]
...
>>>     @property
>>>     def _upstream_addresses(self) -> Dict[str, Set[str]]:
>>>         # a mapping from an upstream "role" to the set of addresses
>>>         # that belong to this upstream
>>>         return {
>>>             "upstream1": {"address1", "address2"},
>>>             "upstream2": {"address3", "address4"},
>>>         }
...
>>>     @property
>>>     def _tls_available(self) -> bool:
>>>         # return if the Nginx config should have TLS enabled
>>>         pass
...
>>>     def _reconcile(self):
>>>         if self._container.can_connect():
>>>             new_config: str = self._nginx.get_config(self._upstream_addresses,
>>>               self._tls_available)
>>>             should_restart: bool = self._has_config_changed(new_config)
>>>             self._container.push(self.config_path, new_config, make_dirs=True)
>>>             self._container.add_layer("nginx", self.layer, combine=True)
>>>             self._container.autostart()
...
>>>             if should_restart:
>>>                 logger.info("new nginx config: restarting the service")
>>>                 self.reload()
...
>>>     def _nginx_upstreams(self) -> List[NginxUpstream]:
>>>         # UPSTREAMS is a list of backend services that we want to route traffic to
>>>         for upstream in UPSTREAMS:
>>>             # UPSTREAMS_PORT is the port the backend services are running on
>>>             upstreams.append(NginxUpstream(upstream, UPSTREAMS_PORT, upstream))
>>>             return upstreams
...
>>>     def _server_ports_to_locations(self) -> Dict[int, List[NginxLocationConfig]]:
>>>         # NGINX_PORT is the port an nginx server is running on
>>>         # Note that you can define multiple server directives,
>>>         # each running on a different port
>>>         return {NGINX_PORT: self._nginx_locations}
otel_module_path = '/etc/nginx/modules/ngx_otel_module.so'
get_config(
upstreams_to_addresses: dict[str, set[str]],
listen_tls: bool,
root_path: str | None = None,
tracing_config: NginxTracingConfig | None = None,
) str

Render the Nginx configuration as a string.

Parameters:
  • upstreams_to_addresses – A dictionary mapping each upstream name to a set of addresses associated with that upstream.

  • listen_tls – Whether Nginx should listen for incoming traffic over TLS.

  • root_path – If provided, it is used as a location where static files will be served.

  • tracing_config – Tracing configuration.

class NginxLocationConfig(
path: str,
backend: str | None = None,
backend_url: str = '',
headers: dict[str,
str]=<factory>,
modifier: Literal['',
'=',
'~',
'~*',
'^~']='',
is_grpc: bool = False,
upstream_tls: bool | None = None,
rewrite: list[str] | None = None,
extra_directives: dict[str,
~typing.Any]=<factory>,
)

Bases: object

Represents a location block in a Nginx configuration file.

For example:

NginxLocationConfig(
    '/',
    'foo',
    backend_url="/api/v1"
    headers={'a': 'b'},
    modifier=EXACT,
    is_grpc=True,
    use_tls=True,
)

would result in the nginx config:

location = / {
    set $backend grpcs://foo/api/v1;
    grpc_pass $backend;
    proxy_connect_timeout 5s;
    proxy_set_header a b;
}
path: str

The location path (e.g., ‘/’, ‘/api’) to match incoming requests.

backend: str | None = None

The name of the upstream service to route requests to (e.g. an upstream block).

backend_url: str = ''

An optional URL path to append when forwarding to the upstream (e.g., ‘/v1’).

headers: dict[str, str]

Custom headers to include in the proxied request.

modifier: Literal['', '=', '~', '~*', '^~'] = ''

The Nginx location modifier.

is_grpc: bool = False

Whether to use gRPC proxying (i.e. grpc_pass instead of proxy_pass).

upstream_tls: bool | None = None

//) If None, it will inherit the TLS setting from the server block that the location is part of.

Type:

Whether to connect to the upstream over TLS (e.g., https

Type:

// or grpcs

rewrite: list[str] | None = None

Custom rewrite, used i.e. to drop the subpath from the proxied request if needed. Example: [‘^/auth(/.*)$’, ‘$1’, ‘break’] to drop /auth from the request.

extra_directives: dict[str, Any]

Dictionary of arbitrary location configuration keys and values. Example: {“proxy_ssl_verify”: [“off”]}

class NginxMapConfig(
source_variable: str,
target_variable: str,
value_mappings: dict[str, list[str]],
)

Bases: object

Represents a map block of the Nginx config.

Example:

NginxMapConfig(
    source_variable="$http_upgrade",
    target_variable="$connection_upgrade",
    value_mappings={
        "default": ["upgrade"],
        "": ["close"],
    },
)

will result in the following map block:

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}
source_variable: str

Name of the variable to map from.

target_variable: str

Name of the variable to be created.

value_mappings: dict[str, list[str]]

Mapping of source values to target values.

class NginxPrometheusExporter(
container: Container,
nginx_port: int = 8080,
nginx_insecure: bool = False,
nginx_prometheus_exporter_port: int = 9113,
)

Bases: object

Helper class to manage the nginx prometheus exporter workload.

reconcile()

Configure pebble layer and restart if necessary.

property layer: Layer

Return the Pebble layer for Nginx Prometheus exporter.

class NginxTracingConfig(
endpoint: str,
service_name: str,
resource_attributes: dict[str,
str]=<factory>,
)

Bases: object

Configuration for OTel tracing in Nginx.

endpoint: str
service_name: str
resource_attributes: dict[str, str]
class NginxUpstream(
name: str,
port: int,
address_lookup_key: str | None = None,
ignore_address_lookup_key: bool = False,
)

Bases: object

Represents metadata needed to construct an Nginx upstream block.

name: str

Name of the upstream block.

port: int

Port number that all backend servers in this upstream listen on.

Our coordinators assume that all servers under an upstream share the same port.

address_lookup_key: str | None = None

Group that this upstream belongs to.

Used for mapping multiple upstreams to a single group of backends (loadbalancing between all). If you leave it None, this upstream will be routed to all available backends (loadbalancing between them).

ignore_address_lookup_key: bool = False

If True, overrides address_lookup_key and routes to all available backend servers.

Use this when the upstream should be generic and include any available backend.

TODO: This class is now used outside of the context of pure coordinated-workers. This arg hence must be renamed to have a more generic name for eg. ignore_address_lookup. See: https://github.com/canonical/cos-coordinated-workers/issues/105

class TLSConfig(server_cert: str, ca_cert: str, private_key: str)

Bases: object

TLS configuration.

server_cert: str
ca_cert: str
private_key: str
class TLSConfigManager(
container: ops.Container,
update_ca_certificates_on_restart: bool = True,
)

Bases: object

TLSConfigManager.

KEY_PATH = '/etc/nginx/certs/server.key'
CERT_PATH = '/etc/nginx/certs/server.cert'
CA_CERT_PATH = '/usr/local/share/ca-certificates/ca.cert'
reconcile(tls_config: TLSConfig | None)

Reconcile container state.

property is_tls_enabled: bool

Return True if the certificates files are on disk.