Copyright @ Lenovo US
Juju charms are, charming. It promises a selection of blueprints that hold magic to make an application deployment easy. But devils are in the details, as always the case. In this article we will walk in the charm world to learn its design.
Bundle, charm, service, application
Juju terms can be confusing. Approximately they can be viewed in a tree model, where a bundle can have multiple charms, a charm can contain multiple services, a service is composed of one or more applications and a group of relations, and a relation, if defined, must be one of the two types:
- provide (eg. I am MySQL and I provide a SQL database relation).
- require (eg. I'm Mediawiki and I require a SQL database).
In term of deployment, a service can be deployed to more than one machine so as to achieve HA; a machine can also have more than one services. Here we use the term machine broadly because it can also be a single LXD container, so in this case there can be many containers in a single VM, and many VMs on a single physical machine.
Charm and layers
How does a charm describe an application and its relations then? The anwser is: layers. A charm is built from three layers: basic/runtime layer, interface layer, and charm layer. Basic layer is like a library — common parts that many applications can use without conflicts. Thing like DB services, HTTP server, storage should belong to this category. Interface layer is an abstract of what a charm exposes for consumption. Case in point will be any database charm, who should always define an interface to exchange DB login credentials and ports. Charm layer is application specific, and its logic is following the reactive pattern.
Hooks and Reactive pattern
Previously, charms are written as a list of hooks responding to events. This is still true because hooks are the actual scripts. Any charm supports the following hooks:
relation defined, there are four relation hooks:
Hooks run in a paritcular sequence, and that sequence is
hardcoded. This is terrible. So instead of individual hooks, we
have now a better way to capture this concept using States
— reactive pattern. A charm can define arbitrary state,
say foo. Another layer in the same charm can then uses Python
@when_not, to specify the status of
foo, which then determined whether the function will be
executed. I suppose the framework itself handles event polling,
broadcasting and the like.
State has a namespace per charm per unit. Therefore, states do not go across unit boundary, nor charm boundary. For states between charms/units, use
There are two ways to construct basic layer: use an existing one, such as the apache-php, or use the basic layer template to write your own. Don't forget to check out these existing runtime layers for reuse. Note: which one can be reused, and which one can not? I don't see any document to distinguish this.
Interface layer defines both the provides and the requires. One thing to remember is that relation does not define the actual connection at all! What it does is to pass along configurations — IP address, port, user name, password, and such. For example, if there is a MySQL provides and Wordpress requires, Wordpress will read from the providing side of configurations needed to make the connection.
Interfaces are categorized into three communication scopes: global level, service level, and unit level. Again, they are grouping concept, where unit is the atom (and the default level). Multiple units can belong to a service level, and everything else goes into global level.
The communication scope is closely related to the relationship lifecycle, where units in the same scope will have data broadcasted to them so they will be aware of each other's state. Therefore, unit level can be used for application that provides no relation and has no peer-to-peer need; service level is certainly providing some type of relation so others can leverage; and global is just a catch-all scope.
Relation and Interface
So how does relation and interface come into play? Declaring relation is a strong statement. As we have already mentioned, there are two types of relations: provides and requires. The provides and requires keys defined in metadata.yaml are used to define pairings of charms that are likely to be fruitful. Consider mongodb's metadata:
name: mongodb ... provides: database: interface: mongodb
where in another metadata who is on the receiving end of this relation:
name: my-node-app ... requires: database: interface: mongodb provides: website: interface: http
The word database is the relation name. There is no restriction to it except it can not be juju or jujud. Relation name serves as a grouping because each relation can have more than one interface.
The interface name, mongodb, is important, since it is being used to look up an existing relation registry for a match. I think this is how charm build can pull in dependencies of other charms/layers.
This is where the application logics live.
See the process in action:
— by Feng Xia