Dhall: Degrees of Correctness or Temperature?

Degrees of Correctness With Dhall

At meshcloud we are huge fans of infrastructure-as-code, GitOps and declarative approaches in general. This means we manage a lot of declarative files in our Git repository and these files get processed automatically when they change. Ideally we want our CI system to make sure that changes we’re making to these files won’t cause any problems once they reach a staging or production environment.

Some tools like Terraform allow you to run validation on your declarative definitions but it’s very likely that you’re also dealing with some JSON or YAML files (e.g. CI pipeline definitions, application configurations, Kubernetes manifests, etc.) that are required by an application that does not provide any way to check these files beforehand.

Let’s have a look how we can use Dhall to solve this problem and even improve on the limitations of formats like JSON or YAML.

Syntactic Correctness in Dhall

As a first step we need to make sure that any files we check in are syntactically correct, this can also be achieved by using something like a JSON/YAML linter but if we have everything as Dhall files we can get by with one tool and get some added benefits like being able to share code via imports, define constants, and use functions.

Let’s see an example, here is a YAML file containing configuration for an application:

roles:
  - name: admins
    permissions: admin
    priority: 100
  - name: developers
    permissions: editor
    priority: 10

Okay, there is a list of roles and each group has a name, permissions and a priority value. Now let’s make a change, we’ll add another role.

roles:
  - name: admins
    permissions: admin
    priority: 100
  - name: developers
    permissions: editor
    priority: 10
  - name: guests
   permissions: viewer
    priority: 1

Can you spot the mistake? By removing a single space before permissions we’ve now created an invalid YAML file so our application won’t be able to parse it and will crash!

Let’s write the same config in Dhall instead:

{ roles =
  [ { name = "admins", permissions = "admin", priority = 100 }
  , { name = "developers", permissions = "editor", priority = 10 }
  , { name = "guests", permissions = "viewer", priority = 1 }
  ]
}

Running this file through Dhall (dhall --file config.dhall) will let us know about any syntactic errors and if everything is correct we can be sure that we can use dhall-to-yaml to generate a valid YAML file for our application later (e.g. as part of our CD process).

Type Correctness in Dhall

So far so good, but what about other kinds of mistakes, for example:

{ rolos =
  [ { name = "admins", permissions = "admin", priority = 100 }
  , { name = "developers", permissions = "editor", priority = 10 }
  , { name = "guests", permissions = "viewer", priority = 1 }
  ]
}

Looks like we misspelled roles but Dhall does not complain since everything is syntactically correct!

To ensure that this does not happen we need to tell Dhall about the shape of our configuration. We do this by specifying a type in other languages this may be called a schema or a spec. It looks like this:

{ roles : List { name : Text
               , permissions : Text
               , priority : Natural
               }
}

Alright, instead of assigning values with = we’re now specifying types with :. Roles is a List with an inner type for the list members and this inner type has two text fields name and permissions and a field priority which should contain a natural number.

We can clean this up a bit by pulling the definition for the inner type into a let binding (basically a constant). By convention, types are given upper case names so let’s call the inner type Role:

let Role = { name : Text
           , permissions : Text
           , priority : Natural
           }

in  { roles : List Role }

This is the same definition as before just a bit more readable.

Now that we have our type written out we can combine it with our actual value. There are different ways to go about this but we’ll keep it simple and add a type annotation to our values directly:

let Role = { name : Text
           , permissions : Text
           , priority : Natural
           }

let Config = { roles : List Role }

in    { roles =
        [ { name = "admins", permissions = "admin", priority = 100 }
        , { name = "developers", permissions = "editor", priority = 10 }
        , { name = "guests", permissions = "viewer", priority = 1 }
        ]
      }
    : Config

First we move our type definition into its’ own let binding and call it Config. Then we add the same config values as before but we make sure to add an annotation of : Config which tells Dhall about the type this value should conform to.

Running it through dhall-to-yaml will yield the same result as before:

$ dhall-to-yaml --file config.dhall
roles:
  - name: admins
    permissions: admin
    priority: 100
  - name: developers
    permissions: editor
    priority: 10
  - name: guests
    permissions: viewer
    priority: 1

Introducing the same mistake as before will result in an error though:

let Role = { name : Text
           , permissions : Text
           , priority : Natural
           }

let Config = { roles : List Role }

in    { rolos =
        [ { name = "admins", permissions = "admin", priority = 100 }
        , { name = "developers", permissions = "editor", priority = 10 }
        , { name = "guests", permissions = "viewer", priority = 1 }
        ]
      }
    : Config
$ dhall --file config.dhall
Use "dhall --explain" for detailed errors

Error: Expression doesn't match annotation

{ - roles : …
, + rolos : …
}

 8│       { rolos =
 9│         [ { name = "admins", permissions = "admin", priority = 100 }
10│         , { name = "developers", permissions = "editor", priority = 10 }
11│         , { name = "guests", permissions = "viewer", priority = 1 }
12│         ]
13│       }
14│     : Config

config.dhall:8:7

As we expected the expression (our value) doesn’t match the annotation (our type), - roles tells us that the roles field is missing (hence the minus sign) and + rolos tells us that there was an unexpected additional field called rolos.

Great, we’ve achieved type correctness, our configuration will definitely be valid and every field will have the correct type!

Semantic Correctness

We’re not done yet though, what happens when we do something like this:

let Role = { name : Text
           , permissions : Text
           , priority : Natural
           }

let Config = { roles : List Role }

in    { roles =
        [ { name = "admins", permissions = "admin", priority = 100 }
        , { name = "developers", permissions = "editor", priority = 10 }
        , { name = "guests", permissions = "read-only", priority = 1 }
        ]
      }
    : Config

We’ve changed the guest role permissions to "read-only", it’s still a text value so our types are in order but when we feed this to our application we get an error:

$ my-app config.yml
ERROR: unknown permissions "read-only" expected one of "admin", "editor", "viewer"

Oh no, permissions isn’t actually a text value it’s an enum! There is no way to encode this in a YAML file so our application reads the text and tries to map it to an enum value of the same name. Even though all our types are in order we’ve failed to provide correct data because the underlying format (YAML) is not expressive enough to specify values that are semantically correct.

Luckily it’s very easy to cover this exact case with Dhall thanks to union types. Union types are used if you have a value that can be of different types and the simplest way of using it is to specify a type that consists of different specific text values.

We create a new Permissions union type as a let binding and use it in our definition of Role:

let Permissions = < admin | editor | viewer >

let Role = { name : Text
           , permissions : Permissions
           , priority : Natural
           }

Between the angle brackets < ... > we have the different values we want to allow (called constructors) separated by pipes |.

Since we’re no longer working with text values we also need to update our values. Instead of using text values like "admin" we write Permissions.admin. Here’s the full example:

let Permissions = < admin | editor | viewer >

let Role = { name : Text
           , permissions : Permissions
           , priority : Natural
           }

let Config = { roles : List Role }

in    { roles =
        [ { name = "admins", permissions = Permissions.admin, priority = 100 }
        , { name = "developers", permissions = Permissions.editor, priority = 10 }
        , { name = "guests", permissions = Permissions.viewer, priority = 1 }
        ]
      }
    : Config

By writing it in the form Type.constructor we can differentiate between alternatives of the same name, for example we could also have a Role.admin.

If we use a non-existent role like Permissions.read-only we’re greeted with an appropriate error:

$ dhall --file config.dhall
Use "dhall --explain" for detailed errors

Error: Missing constructor: read-only

11│                                            Permissions.read-only

config.dhall:11:44

Dhall error messages can be a bit hard to read but in this case the --explain flag does a pretty nice job:

$ dhall --explain --file config.dhall                                                                                                                                                                                                                          ~

Permissions : Type
Role : Type
Config : Type

Error: Missing constructor: read-only

Explanation: You can access constructors from unions, like this:

    ┌───────────────────┐
    │ < Foo | Bar >.Foo │  This is valid ...
    └───────────────────┘

... but you can only access constructors if they match an union alternative of
the same name.

For example, the following expression is not valid:

    ┌───────────────────┐
    │ < Foo | Bar >.Baz │
    └───────────────────┘
                    ⇧
                    Invalid: the union has no ❰Baz❱ alternative

You tried to access a constructor named:

↳ read-only

... but the constructor is missing because the union only defines the following
alternatives:

↳ < admin | editor | viewer >

────────────────────────────────────────────────────────────────────────────────

11│                                            Permissions.read-only

config.dhall:11:44

Now we’ve done pretty much all we can do to ensure that our configuration is as correct as possible and by using Dhall our CI system can inform us about such problems before they turn into failing deployments or runtime errors. Of course we can’t catch all possible errors before we actually run an application (e.g. we specify a wrong but still valid email address) but we should try to catch problems as early as possible.

Where to Go From Here

We’ve only scratched the surface!

  • Union types are much more expressive and each alternative can contain a full other type not just a fixed value.
  • Dhall can automatically embed type information in generated JSON/YAML.
  • Using packages and default values.
  • You can bypass more limitations of JSON/YAML by using Dhall to write you configuration in a way that makes sense semantically and then use a function to generate one or multiple output files.

Cloud Foundry Fail Over

Sie gehören zu den wohl meist gefürchtetsten Dingen im Geschäftsalltag und doch hatte schon jeder mit ihnen zu tun: Serverausfälle. Fehler sind menschlich und so ist kein Rechenzentrum der Welt vor Ihnen gefeit. Irgendwann trifft es jeden mal. Doch wer das Problem und den damit verbundenen Verlust von Umsätzen, den unzufriedenen Kunden und den monetären Schaden als höhere Gewalt abtut, der urteilt wohlmöglich etwas zu voreilig: Mit einer Fail-Over-Routine ist es eine Sache von wenigen Minuten Ihre Applikation von einem ins andere Rechenzentrum zu verlagern. Mit unseren bislang zwei Standorten und unserem meshStack bringen wir schon alles mit um Applikationen ausfallsicher und redundant zu gestalten, ohne sich ans Zeichenbrett setzen zu müssen, um die Infrastruktur neu zu denken. Im Fall der Fälle kann der Standort einer Applikation binnen weniger Minuten verlagert werden, ohne das Ressourcen dauerhaft geo-redundant gehalten werden müssen.  Dabei geht nicht nur der Umzug schnell von statten, sondern auch die Einrichtung. Damit bringen wir Hochverfügbarkeit in jedes Unternehmen, ob One-Man-Show, oder Großkonzern. Hochkritische Infrastrukturen können auch dauerhaft geo-redundant aufgesetzt werden, dies behandeln wir hier nicht.

Drei Schritte zum Seelenfrieden

  1. Automatisches Backup Ihrer Datenbank einrichten
  2. Infrastruktur in einer anderen Location replizieren
  3. Backup einspielen

Vorab: DNS TTL

Wer seine Infrastruktur schnell ersetzen möchte, der benötigt natürlich auch einen flexiblen DNS-Eintrag. Um einen möglichst problemlosen und schnellen Übergang zu gewährleisten, ist es notwendig eine möglichst niedrige TTL (Time To Live) einzustellen. Der TTL-Eintrag definiert wie lange die entsprechenden Domains im DNS Cache gehalten werden.

Automatisches Backup einrichten

An dieser Stelle gehe ich davon aus, dass Sie Ihre Applikation schon in Cloud Foundry deployed haben.

Zunächst loggen Sie sich in der Cloud Foundry CLI ein. Verschaffen Sie sich einen Überblick über die laufenden Services.

cf services

Aus der Liste der laufenden Services suchen Sie sich nun den Datenbank Service aus, den Sie replizieren möchte.

cf service NAME

Nehmen Sie nun das oben genannte Kommando und ersetzen Sie "NAME" mit dem Namen Ihres Services. Cloud Foundry sollte Ihnen nun allerlei Details zu dem spezifizierten Service zurückgeben. Tatsächlich interessiert uns der erste Abschnitt aber am meisten. Er sollte in etwa so aussehen:

Service instance: todo-db
Service: PostgreSQL
Bound apps: todo-backend
Tags:
Plan: S
Description: PostgreSQL Instances
Documentation url:
Dashboard: https://postgresql.cf.eu-de-darz.msh.host/v2/authentication/************

Das Objekt unserer Begierde ist der Dashboard Eintrag. Wir kopieren die URL in unserer Browserfenster. So landen wir im Backup Manager der Datenbank. Hier haben wir die Möglichkeit zyklische Backups der Datenbank in einem Swift Container zu speichern. OpenStack Swift, ist unser Service für Object Storage. Er eignet sich hervorragend zu Ablegen von Dateien wie dem Datenbank-Dump. Die Erstellung eines Swift Containers ist kinderleicht. Navigieren Sie im Service-Menu des meshPanels unter Storage zum Punkt Objects und geben Sie den Namen des Containers ein. Wichtiger Hinweis: Da Sie im Falle eines Ausfalls in einer Location auch der Swift Storage betroffen sein kann, erstellen Sie das Swift Container in der anderen Location.

Nun, wo Sie mit einem Swift Container ausgestattet sind, müssen wir diesen noch im Backuptool hinterlegen. Das Backuptool benutzt die Swift API und genau dafür holen wir uns im meshPanel jetzt die Zugangsdaten ab. Wählen Sie im meshPanel die Location in dem Sie Ihren Swift Container erstellt haben und navigieren Sie zum untersten Punkt in der Seitenleiste "Service User". Dort geben Sie eine Beschreibung ein und wählen aus dem Dropdown-Menu Openstack. Der Service User wird erstellt, und der Browser läd ihnen eine .txt-Datei mit den Login Credentials herunter. Achtung: Die Credentials werden einmalig ausgehändigt, speichern sie diese gut oder erstellen Sie bei Verlust einen neuen Service User.

In dieser Textdatei sind nun alle nötigen Informationen vorhanden. Wir wechseln nun also wieder zum Datenbank Dashboard. Hier wählen wir unter dem Punkt "Backend Endpoints" die Schaltfläche "Create File Endpoint". Öffnen Sie die, im vorherigen Schritt erstellte, Textdatei. Um das ganze etwas abzukürzen mache ich eine kleine Gegenüberstellung welche Elemente aus der Textdatei (links) in welche Felder im Webinterface (rechts) gehören.

API Endpoint --> Authentification URL
Username --> Username
Password --> Password
OS_USER_DOMAIN_NAME --> Domain
OS_PROJECT_NAME --> Project

Abschließend geben Sie den Namen des Containers an, den Sie in einem der vorangegangenen Schritte erstellt haben.

Backup Plan erstellen

Nun haben wir alle Komponenten zusammengeführt um Backups zu erstellen. Abschließend möchten wir noch einen Backup Plan erstellen, der periodisch Backups unserer Datenbank in den gerade verlinkten Container speichert.

Dazu gehen wir wieder auf das Datenbank Dashboard und wählen die Schaltfläche "Create Backup Plan" aus. Für die Frequenz des Backups, nutzen Sie die Spring Cron Syntax. Mit dieser Angabe zum Beispiel 0 0 , läuft ihr Backupjob stündlich. Die "Retention Period" gibt an wie viele Backups behalten werden sollen. Der "Retention Style" sagt dabei aus, wie die vorangegangene Eingabe zu interpretieren ist: Wählt man Beispielsweise als "Retention Period" zwei, und als Retention Style "Day" dann bleiben die Backups der letzten beiden Tage erhalten. Im Dropdown Menü "File Destination" wählen Sie den eben verlinkten Swift Container.

Über die Backup Plans und verschiedene File Destinations können Sie die Redundanz des Backups erhöhen und beispielsweise auf Swift Container aus mehreren Rechenzentren gleichzeitig Backupen.

Im Problemfall

Tritt der Fall der Fälle nun ein und das Rechenzentrum in dem Ihre Applikation läuft ist nicht mehr erreichbar, können Sie nun einfach Ihre Infrastruktur im anderen Rechenzentrum replizieren.

Replikation

Dafür loggen Sie sich im meshPanel ein und wählen die alternative Location aus. Sie loggen Sich über die Cloud-Foundry-CLI ein. Zunächst erstellen Sie den Datenbank Service, den Sie für Ihre Applikation benötigen.

cf marketplace #Zeigt die verfügbaren Services im Marketplace
cf create-service MySQL S Name #erstellt den Service MySQL in der Größe S mit dem Namen "Name"

In der neuen Infrastruktur befindet sich nun zunächst nur eine leere Datenbank, welche im nächsten Schritt wiederhergestellt wird.
Neben der Datenbank ist allerdings ebenfalls ihre Applikation notwendig. Da Sie die Applikation ja bereits in der alten Infrastruktur deployed haben, sind alle notwendigen Anpassungen für Cloud Foundry bereits abgeschlossen. Sie navigieren nun also nur noch mit ihrem Terminal in den Ordner in dem die Applikation gespeichert haben und führen das Deployment mittels cf push aus.

Nachdem Ihre Applikation erfolgreich deployed wurde, müssen Sie diese noch mit ihrem frisch erstellten Datenbankservice verbinden. Vorsicht: Häufig sind die Bindings auch schon im "manifest.yml" der Applikation definiert. Achten Sie darauf, dass der Name ihres Datenbankservices mit dem in der manifest.yml übereinstimmt oder ändern Sie das Manifest nachträglich.

cf bind-service myapp mydb

Backup Einspielen

Dann lassen Sie sich mittels bashscript cf service Name die Dashboard URL anzeigen, öffnen das Dashboard, und tragen ihre Credentials aus der ".txt"-Datei des Service Users unter der Rubrik Restore ein. Das Schema wurde weiter oben schon ausführlich beschrieben und ist hier identisch. Abschließend klicken Sie auf Restore. Ihre Datenbank müsste jetzt also auf dem selben Stand sein wie jene, die zur Zeit von einem Ausfall betroffen ist

DNS Einträge ändern

Jetzt wo Ihre Applikation auf einem anderen Server läuft, müssen Sie selbstverständlich auch die DNS Einträge umstellen.


Cloud Meetups in Frankfurt

Die besten Meetups rund um Cloud, DevOps und Continuous Integration in Rhein-Main

Cloud Meetups in Frankfurt

Wer sagt denn, die Frankfurter Startup-Szene hätte nichts zu bieten? Die steigende Anzahl an Startups äußert sich auch im Meetup-Angebot der Region Rhein-Main. Oft sind die Teilnehmerzahlen nicht riesig, ein vertrauter Meetup-Kreis wird den Treffen jedoch nicht zum Nachteil. Umso eher hat man die Chance sich mit der Community bekannt zu machen, trifft Leute mit ähnlichen Fragestellungen, hat die Möglichkeit sich mit den Vortragenden im direkten Gespräch über ihre Erfahrungen mit den Technologien auszutauschen und so wertvolle Tipps und Anregungen mit nach Hause zu nehmen.

Hier eine Auflistung von spannenden Meetups, zu den Themen, die uns aktuell bewegen: Cloud, Docker, DevOps, Cloud Foundry und OpenStack.

Bis zum nächsten Meetup!