Infrastructure Services
=======================
Overview
--------
Smarter creates and manages cloud infrastructure on behalf of its users. Deploying an
:doc:`LLMClient <../../smarter-resources/smarter-llmclient>` creates a DNS record for its
host, and a Kubernetes Ingress whose TLS certificate is issued automatically. Registering a
:doc:`custom domain <../../smarter-resources/smarter-custom-domain>` creates a DNS zone and a
TLS certificate. A self-hosted :doc:`vectorstore <../../smarter-resources/smarter-vectorstore>`
runs in Kubernetes, with a persistent volume for its data. And the platform sends email, for
example to activate an account or to reset a password.
All of this is done through one layer of Python code, the **infrastructure services**, in the
Django app ``smarter.apps.infrastructure``. The rest of the platform never calls a cloud
provider's SDK, such as AWS's boto3. It asks the infrastructure services for what it needs, in
terms that do not depend on the cloud: a DNS zone, a DNS record, a TLS certificate, a
Kubernetes manifest, an email.
.. code-block:: python
from smarter.apps.infrastructure.services import infrastructure
zone, created = infrastructure.dns.get_or_create_zone("example.com")
certificate_id, _ = infrastructure.certificates.get_or_create_certificate("example.com")
infrastructure.certificates.create_validation_records(certificate_id)
infrastructure.kubernetes.apply_manifest(manifest)
infrastructure.email.send_email(subject="Welcome!", body="
Hello
", to="user@example.com", html=True)
Smarter runs on :doc:`Amazon Web Services ` today. Because the platform depends only on
the infrastructure services, supporting another cloud, such as Microsoft Azure, Google Cloud
or DigitalOcean, means adding a cloud provider, not changing the platform.
The Services
------------
``infrastructure`` gives the platform five services:
- ``dns``: DNS zones and records. A zone, for example an AWS Route53 hosted zone, holds the
records of a domain and its subdomains. Zones and records are described by the
provider-independent :class:`~smarter.apps.infrastructure.services.dns.DNSZone` and
:class:`~smarter.apps.infrastructure.services.dns.DNSRecord`.
- ``certificates``: TLS certificates, issued by the cloud provider, for a domain and its
subdomains. The provider issues a certificate once DNS records prove that the domain is
under Smarter's control, so the certificate service creates those validation records with
the DNS service.
- ``kubernetes``: the resources of the platform's Kubernetes cluster, through
`kubectl `__.
- ``email``: the platform's outgoing email, through SMTP. See :doc:`smtp`.
- ``provider``: the cloud provider itself, for example its account identity, and a
description of the Kubernetes cluster for the ``status`` API.
Each service has a ``ready`` property, which is ``True`` once it is authenticated and
connected. Celery tasks that create infrastructure check it first, so that a Smarter
installation without cloud credentials, such as a developer's laptop, skips them rather than
failing.
Cloud Providers
---------------
Some services depend on a cloud, and some do not:
- **DNS and TLS certificates** are implemented by each cloud provider. On AWS, DNS is Route53,
and certificates come from AWS Certificate Manager (ACM).
- **Kubernetes** does not depend on a cloud: only the cluster's credentials do. kubectl works
the same with any cluster, so the Kubernetes service is implemented once, and the cloud
provider contributes only the kubeconfig, for example with ``aws eks update-kubeconfig``.
- **Email** does not depend on a cloud either. Smarter sends it with SMTP, to any SMTP server,
for example AWS Simple Email Service's.
``SMARTER_CLOUD_PROVIDER``, which is ``smarter_settings.cloud_provider``, selects the provider.
It is ``aws`` by default. ``memory`` selects a provider that keeps DNS zones, records and
certificates in memory, for local development without a cloud account, and for the unit tests.
.. code-block:: bash
SMARTER_CLOUD_PROVIDER=aws
A provider implements only a few primitives of the DNS and certificate services, for example
"find a zone", "create a zone", "list a zone's records". The operations that the platform
uses, for example copying the environment's A record to a new LLMClient's host, are built on
those primitives once, in the services, so they behave the same in every cloud.
Adding a Cloud
~~~~~~~~~~~~~~
To add a cloud, for example Azure:
1. Add a package, ``smarter.apps.infrastructure.providers.azure``, with a subclass of
:class:`~smarter.apps.infrastructure.providers.base.CloudProvider`, and subclasses of
:class:`~smarter.apps.infrastructure.services.dns.DNSService` and
:class:`~smarter.apps.infrastructure.services.certificates.CertificateService` that implement
their abstract primitives with the cloud's SDK. Translate the SDK's errors into the
infrastructure exceptions, with the services' ``operation()`` context manager.
2. Register the provider in :mod:`smarter.apps.infrastructure.providers`, under its
:class:`~smarter.apps.infrastructure.const.CloudProviders` name.
3. Set ``SMARTER_CLOUD_PROVIDER=azure``.
The AWS provider, :mod:`smarter.apps.infrastructure.providers.aws`, and the in-memory provider,
:mod:`smarter.apps.infrastructure.providers.memory`, are examples.
Signals
-------
The infrastructure services send Django signals, whichever provider implements them, so that
the platform can observe its infrastructure without knowing which cloud it runs on. Each signal
has the arguments ``service``, for example ``dns``, and ``provider``, for example ``aws``.
- ``infrastructure_authenticated`` and ``infrastructure_authentication_failed``: a provider
authenticated with its cloud, or could not. They are sent when the state changes, rather than
on every check.
- ``infrastructure_connected`` and ``infrastructure_connection_failed``: a service connected to
its backend, for example the Kubernetes cluster, or could not.
- ``billable_resource_creating``, ``billable_resource_created``,
``billable_resource_destroying`` and ``billable_resource_destroyed``: a resource that the
cloud bills for, for example a DNS zone, a persistent volume, or a load balancer, is about to
be, or was, created or destroyed.
- ``resource_created`` and ``resource_destroyed``: a resource that is not billed on its own,
for example a DNS record or a free TLS certificate, was created, updated or destroyed.
- ``resource_applied``: a Kubernetes manifest was applied.
- ``infrastructure_operation_failed``: a service's operation failed.
- ``email_sent`` and ``email_failed``.
Connect a receiver to observe them:
.. code-block:: python
from django.dispatch import receiver
from smarter.apps.infrastructure.signals import billable_resource_created
@receiver(billable_resource_created)
def meter(sender, service, provider, resource_type, resource_name, resource_id, **kwargs):
...
The Resource Ledger
-------------------
Smarter's own receivers log every infrastructure signal, and record each resource that is
created or destroyed in a ledger, the Django model
:class:`~smarter.apps.infrastructure.models.InfrastructureResource`: its provider, service,
type, name, the provider's id for it, whether it is billable, and whether it still exists. The
ledger answers "what has Smarter provisioned in our cloud account, and what is it costing us?"
without asking each cloud.
Superusers see the ledger in the web console, under **Settings, Infrastructure Resources**: a
summary of the active, billable and destroyed resources, and a list that can be filtered by
status, by provider, by whether a resource is billable, and by text. The list is read-only: the
platform writes the ledger as it creates and destroys resources. It is also in the Django admin.
Testing Safely
--------------
The Smarter Docker containers hold real cloud credentials, and a real kubeconfig, which can
create billable resources. So the real services refuse to reach their backends from the unit
tests: the AWS provider is never ready, kubectl never runs, and SMTP sends nothing. Tests
install fakes instead:
.. code-block:: python
from smarter.apps.infrastructure.providers import configure_provider
from smarter.apps.infrastructure.providers.memory import InMemoryProvider
from smarter.apps.infrastructure.services import InMemoryEmailService, configure_email
provider = InMemoryProvider()
configure_provider(lambda: provider)
configure_email(InMemoryEmailService)
...
configure_provider(None)
configure_email(None)
Tests of code that uses ``infrastructure`` can also patch it where it is used, for example
``patch("smarter.apps.llmclient.tasks.verify_custom_domain.infrastructure")``. Tests that must
use real infrastructure are tagged ``infrastructure``, are skipped by default, and allow it
explicitly, for example with ``AWSProvider(allow_in_tests=True)``.
Technical Reference
-------------------
.. toctree::
:maxdepth: 1
infrastructure/services
infrastructure/dns
infrastructure/certificates
infrastructure/kubernetes
infrastructure/email
infrastructure/providers
infrastructure/aws
infrastructure/memory
infrastructure/signals
infrastructure/models
infrastructure/views
infrastructure/exceptions
infrastructure/const