What is a charm?
A charm is the user-installable component at the heart of Juju. It contains a sets of scripts that simplify both the deployment and management of the charm’s application within Juju. The charm store lists hundreds of recommended charms, from Postgresql to Kubernetes alongside of hundreds more created by the community.
This guide will go through the first basic concepts of charm development, using Python 3 as the scripting language. Two further tutorials build on these foundations to create a fully fledged charm.
What you will learn
In this first charm development tutorial, we will cover:
- Preparing and setup of a basic workbench
- Creating the example charm with charm tools
- Understanding the anatomy of a charm (files and directories)
- Validating and building the charm
- Adding functionality via a secondary layer (layer:apt)
- Deploying the example charm with Juju
ⓘ This guide is aimed at people running the latest Ubuntu LTS release who are familiar with the Linux and Juju environments and are comfortable scripting or coding their own solutions.
We’re going to create a basic software stack we’ll call a workbench to build charms. This is what a typical workbench consists of:
- A Juju controller: We’ll use this to deploy developed charms to. See Getting started with Juju for more details.
- Python 3.x: We use python 3 in this tutorial to develop our charm.
- Charm Tools: Used to create skeleton charms and build, fetch and test charms. See the Charm Tools page for installation instructions.
Start by creating three directories for our build environment:
mkdir -p ~/charms mkdir -p ~/charms/layers mkdir -p ~/charms/interfaces
Add the following environment variables to your
export JUJU_REPOSITORY=$HOME/charms export LAYER_PATH=$JUJU_REPOSITORY/layers export INTERFACE_PATH=$JUJU_REPOSITORY/interface
Finally, source your ~/.bashrc to get the environment properly setup.
Create an example charm
The Charm tools package exists to simplify the creation of new charms. Let’s start a new charm that we’ll name
cd ~/charms/layers charm create layer-example
Great work, lets move on to what a charm consists of.
The anatomy of a charm
A bare minimum charm consists of a directory with the charm name and two files,
metadata.yaml. These are the only elements that are strictly required for a charm to be valid. We do, however, normally create a directory called
reactive which should contain a Python module with the same name as our charm. This was done automatically when we ran ‘charm create layer-example’ above.
Let’s examine what was created by the charm command:
layer-example ├── config.yaml <-- Configuration options for our charm/layer. ├── icon.svg <-- A nice icon for our charm. ├── layer.yaml <-- The layers and interfaces we include. ├── metadata.yaml <-- Information about our charm ├── reactive <-- Needed for all reactive charms │ └── layer_example.py <-- The charm code ├── README.ex <-- README └── tests <-- Tests goes in here ├── 00-setup <-- A skeleton setup test └── 10-deploy <-- A skeleton deploy test
ⓘ Prefixing the charm directory name with layer- is a naming convention. It tells us that this charm is a reactive charm.
Validating the charm
If we were to build our charm now, it would fail because it’s created with defaults. We can see this by running “charm proof” to validate our charm structure:
cd ~/charms/layers charm proof layer-example
The output to the above will look something like the following:
I: Includes template icon.svg file. I: no hooks directory W: no copyright file W: Includes template README.ex file W: README.ex includes boilerplate: Step by step instructions on using the charm: W: README.ex includes boilerplate: You can then browse to http://ip-address to configure the service. W: README.ex includes boilerplate: - Upstream mailing list or contact information W: README.ex includes boilerplate: - Feel free to add things if it's useful for users E: template interface names should be changed: interface-name I: relation provides-relation has no hooks E: template interface names should be changed: interface-name I: relation requires-relation has no hooks E: template interface names should be changed: interface-name I: relation peer-relation has no hooks I: missing recommended hook install I: missing recommended hook start I: missing recommended hook stop I: missing recommended hook config-changed
Let’s get rid of these
E: errors by editing the following files:
layer-example/layer.yaml (always include ‘layer:basic’):
includes: - 'layer:basic'
name: example summary: A very basic example charm maintainer: Your Name <firstname.lastname@example.org> description: | This is a charm I built as part of my beginner charming tutorial. tags: - misc - tutorials
from charms.reactive import when, when_not, set_state @when_not('example.installed') def install_example(): set_flag('example.installed')
With those files edited as above, we can now move on to building our charm.
Build the example charm
We are now ready to build our charm. Start by entering the following:
cd ~/charms/layers charm build layer-example
This will generate output similar to this:
build: Composing into /home/erik/charms build: Destination charm directory: /home/erik/charms/trusty/example build: Please add a `repo` key to your layer.yaml, with a url from which your layer can be cloned. build: Processing layer: layer:basic build: Processing layer: example (from layer-example) proof: I: Includes template icon.svg file. proof: W: Includes template README.ex file proof: W: README.ex includes boilerplate: Step by step instructions on using the charm: proof: W: README.ex includes boilerplate: You can then browse to http://ip-address to configure the service. proof: W: README.ex includes boilerplate: - Upstream mailing list or contact information proof: W: README.ex includes boilerplate: - Feel free to add things if it's useful for users proof: I: all charms should provide at least one thing
Great work! Your charm has been assembled and placed in the
$JUJU_REPOSITORY/trusty/example directory. Go ahead and take a look in to it before we move on.
Add functionality with layers
Our example charm isn’t really doing anything fun yet. Let’s update it to install the hello package and set a
Hello World message for Juju once it’s done.
Modify the ~/charms/layers/layer-example/layer.yaml to look like this:
includes: - 'layer:basic' - 'layer:apt' options: apt: packages: - hello
Modify ~/charms/layers/layer-example/reactive/layer_example.py to look like this:
from charms.reactive import set_flag, when, when_not from charmhelpers.core.hookenv import application_version_set, status_set from charmhelpers.fetch import get_upstream_version import subprocess as sp @when_not('example.installed') def install_example(): set_flag('example.installed') @when('apt.installed.hello') def set_message_hello(): # Set the upstream version of hello for juju status. application_version_set(get_upstream_version('hello')) # Run hello and get the message message = sp.check_output('hello', stderr=sp.STDOUT) # Set the active status with the message status_set('active', message ) # Signal that we know the version of hello set_flag('hello.version.set')
Let’s build again with our changes.
cd ~/charms/layers/ charm build layer-example
The charm will now be built and the final charm assembled, ending up in
We can now deploy it with Juju:
juju deploy example
After some time,
juju status will show the “Hello World” message.
Congratulations, you have just created and deployed your first charm!
Layers versus charms
One way of thinking about layers in relation to charms, is in terms of libraries or modules. A compilation of layers results in a charm that can be deployed by the Juju engine.
There are a lot of layers included in the charm tools and you can find them in the layer index that we will cover in the next part of the tutorial.
How to think about ‘Reactive programming’
Most programmers expect their applications to run from a clear main() starting point and to move on, step-by-step, towards an exit. Reactive programming is ‘somewhat’ different in how you plan the execution sequence.
In reactive programming, a good way of thinking about your program is that it has many main() entry points. Which of these is executed, and when, depends on how you act on the different states/flags communicated to you by the Juju engine.
The principle is that the Juju engine sends signals to your application, and you write code/functions to act on this information. Your code then raises new flags/states to communicate with the rest of the system.
This is what the
@when(some.flag.raised) decorators are all about.