Backend

Back-end

The back-end consumes Operations from the queue, and run various commands. The initial engine is ansible-playbook, but Kubernetes is also likely to follow.

Fundamentally, a Backend needs to be able to:

  1. Render one or more template files, substituting in variable values specific to the Operation/Task
  2. Run one or more commands to provision the resources with the rendered templates.

Kubernetes

Over the last couple years, we’ve had the opportunity to work on deploying complex, stateful web applications (Django, Drupal) in a Kubernetes environment. We’ve defined a general framework to support our development and production hosting workflows, and recognized this as a solid basis for an alternate backend to plug in to the existing Aegir5 front-end.

As shown in the diagram below, traffic enters the system via the router that directs traffic to the cluster. The router exposes a public IP address, to which we point our DNS entries. From there the K8S ingress service directs traffic within the cluster.

In addition, the ingress controller automatically generates Let’s Encrypt HTTPS certificates, and acts as the HTTPS endpoint, handling the TLS handshake, etc.

Traffic gets directed to the Drupal deployment, which in turn, connects to the database deployment. These are running Docker containers. In addition, temporary job containers can be launched to perform specific tasks, such as installing the site, or running cron.

In the case of the Drupal containers, we’re running a custom Drupal image that bakes our code base in. The database deployment, for its part, is using a stock mariadb image.

Our custom Drupal image, by including the project code base, provides us with reproducible builds. We can deploy this same image to multiple environments with confidence that it will be consistent across all of them.

Both the database, and the Drupal site are connected to storage via Persistent Volume Claims (PVCs). More on this later.

Ansible roles

Currently we write small Ansible roles to represent each task we want to implement, rather than the usual model of handling all tasks related to a given application. A small set of variables will need to be mapped to the equivalent front-end fields.

This is somewhat tedious, and we’re likely to refactor this so that frontend Task maps to a task within an Ansible role, so a Role roughly maps to an Operation.

Backend implementation (examples)

Ansible-playbook queue worker
  1. Celery queue worker receives task.
  2. Writes temporary playbook (see example below) populated with vars and roles from the originating task.
  3. Runs playbook.
  4. Posts log output to front-end URL, where it populates the task log.
  5. Roles may receive a “feedback_url” variable, whence it can post data required by the front-end (e.g., backup path, git commit hash, etc.)
    1. Role is responsible for a cURL (or whatever) call to post data to the provided URL.
  6. Cleans up playbook.
Temporary playbook example
  - hosts: web0
    vars:
       ( populated from task )
    roles:
       ( populated from task )
Ansible role examples

“Task” roles are small re-usable pieces of config.

aegir.GenerateDrupalSiteBackup/
├── meta/main.yml (equiv. to .info)
├── defaults/main.yml (variable defaults)
└── tasks/main.yml (steps to execute)
    ├── drush archive-dump
    ├── move to storage location (path)
    └── post path back to front-end
aegir.WriteNginxVhost/
├── meta/main.yml
├── files/default-nginx-vhost.conf.j2
├── defaults/main.yml
└── tasks/main.yml
    ├── write vhost from template (possibly looking for overrides in various places)
    └── restart nginx (but only if the vhost changed; i.e. idempotence)