Feature Flags with Unleash Python SDK

Introduction

This tutorial guides you through integrating feature flags into any python project using the Unleash Python SDK v5.0. Feature flags (or feature toggles) allow you to modify system behaviour without changing code and redeploying. Note that ska-tango-examples is chosen as an example project, but the same principles apply to any project. This is useful for:

  • Gradual/Compatible rollouts of new features.

  • A/B testing different implementations.

  • Enabling/disabling features for specific users, environments, or data centres.

  • Quickly disabling problematic features (“kill switch”).

Read more on Feature Flags for a detailed explanation of what feature flags are, why we want to use them and how they fit in to the software development lifecycle at the SKAO and to decide if and how you want to use them and whether this tutorial is right for you. This tutorial is for you if you have answered “Yes” to both questions in the flowchart.

We will focus on setting up the Unleash client in a configurable way suitable for deployments across multiple data centres. We will be adding two feature flags, one for Timer and one for the Event Generator using tango-utils chart and generic implementation that is valid for any python project.

Prerequisites

  • Python 3.10 or later (as specified in pyproject.toml).

  • Poetry for dependency management. If you are using a different package manager, you will need to adjust the pyproject.toml file accordingly and use the equivalent commands to install the dependencies for the rest of the tutorial.

  • Docker

  • Helm (v3) and a Kubernetes cluster (like Minikube or an SKAO cluster).

  • Access to an Unleash Server instance (either self-hosted, proxy or GitLab Feature Flags). You will need:

    • The Unleash server URL. (UNLEASH_URL)

    • The Unleash Instance ID. (UNLEASH_INSTANCE_ID)

  • Example feature flags to be added: * ska-tango-examples-timer-double-increment: A flag to enable or disable a double increment feature. * ska-tango-examples-event-generator: A flag to enable or disable a beta feature.

For the Unleash server information, please refer to the Gitlab documentation on Feature Flags and follow the get access credentials section.

Installation

Add the Unleash Python SDK as a dependency to the project using Poetry.

poetry add UnleashClient

This will update the pyproject.toml and poetry.lock files. Ensure you commit these changes.

Configuration

To support multiple deployment environments (e.g., different data centres, staging vs. production), we should make the Unleash client configuration customisable, preferably via environment variables.

Key configuration parameters include:

  • UNLEASH_URL: The URL of the Unleash API server.

  • UNLEASH_APP_NAME: A name identifying the application (e.g., ska-tango-examples).

  • UNLEASH_ENVIRONMENT: The current environment (e.g., development, staging, production, data-centre-1). This should match an environment defined in the Unleash server.

  • UNLEASH_INSTANCE_ID: A unique identifier for this specific instance of the application (useful for stickiness and metrics). Can be hostname or pod name.

  • UNLEASH_REFRESH_INTERVAL: How often (in seconds) the client fetches updated flag definitions (default: 15).

  • UNLEASH_METRICS_INTERVAL: How often (in seconds) the client sends usage metrics to the server (default: 60). It’s advised to set disable_metrics=False in the client initialisation to disable metrics collection.

  • UNLEASH_CUSTOM_STRATEGIES: (Optional) Path to a file defining custom activation strategies.

These can be set directly in the deployment environment (e.g., Kubernetes ConfigMaps or Secrets) or loaded from a .env file during local development.

Dockerfile Configuration

While you can set default values using the ENV instruction in the Dockerfile, it’s generally not recommended for configuration that changes between environments or for sensitive data like API tokens. Defaults are mainly useful for local development or non-sensitive, rarely changing parameters.

# In Dockerfile

# ... (previous build stages) ...

FROM artefact.skao.int/ska-python:0.1.1

# Adding the virtualenv binaries to the PATH
ENV VIRTUAL_ENV=/app/.venv
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

COPY --from=build ${VIRTUAL_ENV} ${VIRTUAL_ENV}
COPY --from=tools /usr/local/bin/retry /usr/local/bin/retry
COPY --from=tools /usr/local/bin/wait-for-it.sh /usr/local/bin/wait-for-it.sh
COPY src /app/src

# Add source code and venv packages to PYTHONPATH
ENV PYTHONPATH="/app/src:app/.venv/lib/python3.10/site-packages/:${PYTHONPATH}"

# --- Unleash Configuration ---
# Set defaults (optional, prefer runtime injection via Helm/K8s)
ENV UNLEASH_APP_NAME="ska-tango-examples"
ENV UNLEASH_ENVIRONMENT="development"
# ENV UNLEASH_URL="https://gitlab.com/api/v4/feature_flags/unleash/9673989 # Make sure you get the project ID right
# ENV UNLEASH_REFRESH_INTERVAL="15"
# ENV UNLEASH_METRICS_INTERVAL="60"

# --- IMPORTANT ---
# DO NOT set secrets here. Inject it securely at runtime.
# UNLEASH_URL, UNLEASH_ENVIRONMENT, UNLEASH_INSTANCE_ID should ideally
# also be set at runtime for flexibility across deployments.

# ... (rest of Dockerfile) ...

The primary way to configure the running container will be through Kubernetes manifests, managed by Helm.

Helm Chart Configuration

We will manage the Unleash configuration within the ska-tango-examples Helm chart.

  1. Define Values in `values.yaml`: Add a section to charts/ska-tango-examples/values.yaml to hold Unleash configuration.

    # charts/ska-tango-examples/values.yaml
    
    # ... (other values) ...
    
    unleash:
      # URL of the Unleash server API
      url: "https://gitlab.com/api/v4/feature_flags/unleash/9673989"
      appName: "ska-tango-examples"
      # Environment name (should match Unleash environment)
      environment: "development"
      # Refresh interval in seconds
      refreshInterval: 15
      # Name of the Kubernetes secret containing the Unleash Instance ID
      idSecretName: "ska-tango-examples-unleash-id"
      # Key within the Kubernetes secret that holds the Unleash Instance ID
      idSecretKey: "unleash-instance-id"
    
  2. Manage the API Token Secret using Vault Secrets Operator: The API token should be stored securely in HashiCorp Vault. The Vault Secrets Operator (VSO) will be responsible for syncing this token into a standard Kubernetes Secret within the application’s namespace.

    You need to define a VaultStaticSecret custom resource in Kubernetes. This resource tells VSO:

    • Which path in Vault contains the Unleash Instance ID (e.g., skao-team-system/ska-tango-examples).

    • Which key within that Vault path holds the Instance ID (e.g., unleash-instance-id).

    • What to name the Kubernetes Secret that VSO will create/manage (this should match unleash.idSecretName from the values.yaml, e.g., unleash-secrets).

    • What key to use within the managed Kubernetes Secret (this should match unleash.idSecretKey from the values.yaml, e.g., unleash-instance-id).

    Example VaultStaticSecret resource (this definition might exist elsewhere in your infrastructure configuration) under templates/unleash-secret.yaml:

    apiVersion: secrets.hashicorp.com/v1beta1
    kind: VaultStaticSecret
    metadata:
      name: unleash-secret # Name for the VSO resource itself
    spec:
      type: kv-v2
      mount: dev
      path: skao-team-system/ska-tango-examples
      destination:
        name: unleash-secrets
        create: true
    

    Warning

    Ensure the actual instance id is securely stored only in Vault. The VaultStaticSecret resource itself does not contain the sensitive token.

    Refer to Vault documentation for more details on how to set up the Vault path and the VSO configuration.

    Since this repository is also using ska-tango-utils chart which provides a convenient template to auto-generate the VaultStaticSecret resource, you can use the following generate the resource:

    # Under a device server of your choice that needs the Unleash Instance ID.
    # Here we are using Timer as an example.
    secrets:
    - secretPath: skao-team-system/ska-tango-examples
      secretMount: dev
      env:
      - secretKey: unleash_instance_id
        envName: UNLEASH_INSTANCE_ID
        default: "secret"
    
  3. Inject Environment Variables in Helm Template: Modify the Helm template that defines the deployments to inject the configuration as environment variables.

    {# ... inside the container spec ... #}
    env:
      # --- Add Unleash Environment Variables ---
      - name: UNLEASH_URL
        value: {{ .Values.unleash.url | quote }}
      - name: UNLEASH_APP_NAME
        value: {{ .Values.unleash.appName | quote }}
      - name: UNLEASH_ENVIRONMENT
        value: {{ .Values.unleash.environment | quote }}
      - name: UNLEASH_REFRESH_INTERVAL
        value: {{ .Values.unleash.refreshInterval | quote }}
      # Inject Instance ID (e.g., using Kubernetes secret)
      - name: UNLEASH_INSTANCE_ID
        valueFrom:
          secretKeyRef:
            name: {{ .Values.unleash.idSecretName }}
            key: {{ .Values.unleash.idSecretKey }}
    {# ... (rest of container spec) ... #}
    
  4. Deploy/Upgrade: Deploy or upgrade the Helm release using the updated chart and values.

    # Example using make, assuming values are set correctly
    make k8s-install-chart # or make k8s-upgrade-chart
    

Initialisation

The Unleash client needs to be initialised once when the application (or relevant part of it) starts. For Tango devices, this could be done within the device’s init_device method, but care must be taken to avoid re-initialising unnecessarily or creating multiple clients. A better approach might be a shared utility module or a singleton pattern.

Here’s an example of how to initialise the client using environment variables:

import logging
import os

from UnleashClient import UnleashClient

logger = logging.getLogger(__name__)

class FeatureToggler():
    __allow_reinitialization = False

    def __init__(self):
        # Check if the instance is already initialized
        self.init_feature_toggler()
        self.initialized = True

    def init_feature_toggler(self) -> None:
        """
        Initialize the feature toggler.
        """
        if "UNLEASH_INACTIVE" not in os.environ:
            try:
                self.unleash_client = UnleashClient(
                    url=os.environ["UNLEASH_API_URL"],
                    app_name=os.getenv(
                        "UNLEASH_ENVIRONMENT", default="development"
                    ),
                    instance_id=os.environ["UNLEASH_INSTANCE_ID"],
                    disable_metrics=True,
                    disable_registration=True,
                    verbose_log_level=logger.getEffectiveLevel(),
                    refresh_interval=60,  # Refresh every minute
                )
                self.unleash_client.initialize_client()
                if not self.unleash_client.is_initialized:
                    self.unleash_client.unleash_verbose_log_level = (
                        logging.NOTSET
                    )
            except Exception as e:
                logger.error("Error initializing feature toggler: %s", e)
                self.unleash_client.unleash_verbose_log_level = logging.NOTSET

Then call self._feature_toggler = FeatureToggler().unleash_client in an early part of your device’s initialisation code (e.g., in the constructor or init_device method).

Basic Usage

To check if a feature is enabled, use the is_enabled function of UnleashClient.

from ska_tango_examples.feature_flags import feature_flags

# "timer-double-increment" is the feature flag name
# fallback_function is called if the feature flag is not found,
# returning False by default in this case.
if self._feature_toggler.is_enabled(
         "timer-double-increment", fallback_function=lambda: False
     ):
         self._value += 2
     else:
         self._value += 1

Best Practices

  • Graceful Degradation: Always provide a default behaviour (using fallback_value or checking client.is_initialized) in case the Unleash server is unreachable or the client fails to initialise.

  • Logging: Log feature flag decisions, especially when behaviour changes, to aid debugging.

  • Clean Up: Regularly remove flags from the code once features are fully rolled out or deprecated.

  • Naming Conventions: Use clear, descriptive names for the feature flags.

  • Security: Manage API tokens securely using Kubernetes secrets or other secure mechanisms. Do not hardcode them or commit them to version control.

  • Vault Integration: Ensure VSO has the correct permissions (Vault policies, Kubernetes service account roles) to read the specified Vault path and manage secrets in the target namespace.

Conclusion

You have now learned how to install, configure, initialise, and use the Unleash Python SDK v5.0(v6.0 is not yet supported by GitLab) within the ska-tango-examples project, including configuration via Docker and Helm, leveraging the Vault Secrets Operator for secure API token management. By using environment variables and VSO-managed Kubernetes secrets, this setup is suitable for multi-data centre deployments. Feature flags provide powerful control over application behaviour, enabling safer releases and more flexible operations.

You can also see the MR for the changes made to the ska-tango-examples repository to implement this tutorial here.