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: ~typing.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_tls_port: int = 443,
nginx_prometheus_exporter_port: int = 9113,
nginx_serves_tls: bool = False,
)

Bases: object

Helper class to manage the nginx prometheus exporter workload.

reconcile(
tls_config: TLSConfig | None = None,
nginx_serves_tls: bool = False,
) None

Configure pebble layer and restart if necessary.

property are_certificates_on_disk: bool

Return True if the certificates files are on disk.

This is used to determine whether the exporter should serve metrics over HTTP or HTTPS by checking whether the certificates are present on THIS container’s FS. It has no effect on whether or not the exporter will attempt to scrape nginx over HTTP or HTTPS. That is determined by the nginx_serves_tls parameter passed to the reconciler.

property web_config: str
command(nginx_serves_tls: bool = False) str
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.crt'
reconcile(tls_config: TLSConfig | None)

Reconcile container state.

property is_tls_enabled: bool

Return True if the certificates files are on disk.