[Tutorial] How to build a charm using modern tools


Duration 0:10

This tutorial aims to show you the steps needed to prepare a charm, starting from just the bare needed Python code and ending in the proper Juju deploy, using both Operator Framework and Charmcraft projects.

Create the charm

Duration 05:00

To start a real project, we can rely on charmcraft init to setup the base structures that we’ll need to construct a complete charm: a basic code we can extend (with its correspondent tests!), proper requirements files for our Python dependencies, the required metadata.yaml, other configuration files we may need (like actions.yaml) and other project files (like a README, LICENSE, etc).

But to understand all that info can be overwhelming, so let’s start with a more basic approach: as the bare minimum things we need to start are our charm code and the metadata file, let’s go with that.

For the purposes of this tutorial, we’ll go with a very simple Python code that doesn’t do anything special, just logs that it was installed properly, so we can see that the whole process is done.

Let’s see it, will explain the code below…

#!/usr/bin/env python3

import logging

from ops.charm import CharmBase
from ops.main import main

logger = logging.getLogger(__name__)

class MyCharm(CharmBase):
    def __init__(self, *args):
        self.framework.observe(self.on.install, self.on_install)

    def on_install(self, event):
        logger.info("Congratulations, the charm was properly installed!")

if __name__ == "__main__":

The overall structure is very simple: we have a couple imports (logging is from Python’s stdlib, and ops is the Operator Framework, we’ll see later how to make it available), then the logger at module level, our charm’s class, and the init at the end. Note that the class inherits from CharmBase and then at the end we pass it to the framework’s main: this is all we need to start using the Operator Framework.

In the charm class, after calling parent’s __init__ (very important for the charm initialization), all we do is tell the framework to call our on_install method when the install event is triggered. The hooked method only logs that all succeeded.

Of course Juju also needs a metadata.yaml file with some information about our project:

name: tutocharm
summary: A charm for the tutorial.
description: A simple and basic charm to show the Operator Framework and Charmcraft in a tutorial.

So, I put that content in the respective files (and made the charm.py file executable):

$ tree .
├── metadata.yaml
├── requirements.txt
└── src
    └── charm.py

1 directory, 3 files

You may have spotted the requirements.txt file in that tree. It’s where we can put all the Python dependencies we need. For this tutorial, we just included the Operator Framework, so the content of our requirements file is:


Build the charm

Duration 02:00

We’re ready to build our charm. For this we need the Charmcraft tool. Soon it will be ready as a snap, but for now we can use it from a virtual environment:

$ virtualenv --python=python3 env
$ source env/bin/activate
(env) $ pip install charmcraft
Successfully installed charmcraft-0.0.1

Ready to use charmcraft, then. Let’s build our charm:

(env) $ charmcraft build
Done, charm left in 'tutocharm.charm'

That’s all we need.

Deploy the charm

Duration 02:00

The final step is deploy it, but before, in a different terminal, execute the following to see the logging we specified in the charm:

juju debug-log

Back to the original terminal, let’s deploy our charm:

juju deploy ./tutocharm.charm

Wait some moments to let Juju do its magic, and at some point, in the juju debug-log terminal, a line very similar to the following one will appear:

unit-tutocharm-1: 13:26:10 INFO unit.tutocharm/1.juju-log Congratulations, the charm was properly installed!

That’s all!


Hi @facundo,

This is really cleaner way of starting a new charm indeed thanks to the new “ops” module and the simplified requirements.txt.

I’m not sure where is the central place these days to store the template of the new operator framework charms, but would you mind keeping the charm-tools updated so that everyone who tries charm create -t operator-python can get benefit of the newer and the cleaner method?
(at this moment, it puts https://github.com/canonical/operator.git as a submodule instead of requirements.txt)


Hello @nobuto, glad you like this!

We will have very soon a charmcraft init command that will create directories and files that will be the base structures for a new charm in this new charmcraft/operator world.

Probably people should wait for that command (in the meanwhile creating the dirs by hand as described in this tutorial) and not use that operator_python template from charm-tools, because it’s not doing the right things…


Cool, good to know.

Would have been nicer if the template was managed by the engineering team, but in the meantime, I’ve opened a pull request to deprecate it in the charm-tools.



Hi @facundo, a very clear post!

I had one “problem” following the steps you described, when building the charm. I got that the Charm entry point must be executable:

(env) $ tail /tmp/charmcraft-log-8oz0524w 
2020-10-22 18:46:08,463  charmcraft.guard               DEBUG    Starting charmcraft version 0.5.0
2020-10-22 18:46:08,464  charmcraft.main                DEBUG    Commands loaded: {'help': <charmcraft.main.HelpCommand object at 0x7fbd3c2225e0>, 'build': <charmcraft.commands.build.BuildCommand object at 0x7fbd3c222640>, 'init': <charmcraft.commands.init.InitCommand object at 0x7fbd3c2226a0>, 'version': <charmcraft.commands.version.VersionCommand object at 0x7fbd3c222700>, 'login': <charmcraft.commands.store.LoginCommand object at 0x7fbd3c222760>, 'logout': <charmcraft.commands.store.LogoutCommand object at 0x7fbd3c2227c0>, 'whoami': <charmcraft.commands.store.WhoamiCommand object at 0x7fbd3c222820>, 'register': <charmcraft.commands.store.RegisterNameCommand object at 0x7fbd3c222880>, 'names': <charmcraft.commands.store.ListNamesCommand object at 0x7fbd3c2228e0>, 'upload': <charmcraft.commands.store.UploadCommand object at 0x7fbd3c222940>, 'revisions': <charmcraft.commands.store.ListRevisionsCommand object at 0x7fbd3c2229a0>, 'release': <charmcraft.commands.store.ReleaseCommand object at 0x7fbd3c222a00>, 'status': <charmcraft.commands.store.StatusCommand object at 0x7fbd3c222a60>}
2020-10-22 18:46:08,468  charmcraft.main                DEBUG    Parsed arguments: Namespace(_command=<charmcraft.commands.build.BuildCommand object at 0x7fbd3c222640>, entrypoint=None, from=None, help_command=False, help_global=False, quiet_command=False, quiet_global=False, requirement=None, verbose_command=False, verbose_global=False)
2020-10-22 18:46:08,469  charmcraft                     ERROR    Charm entry point must be executable: '/home/jose/trabajos/canonical/charms/charmtest/src/charm.py' (full execution logs in /tmp/charmcraft-log-8oz0524w)

After that I executed:

(env) $ chmod +x src/charm.py

and re-run the build:

(env) $ charmcraft build
Ignoring symlink because targets outside the project: 'env/bin/python3.8'
Ignoring symlink because targets outside the project: 'env/bin/python3'
Ignoring symlink because targets outside the project: 'env/bin/python'
Done, charm left in 'tutocharm.charm'

At this point I think everything was fine.

When deploying the charm, I got a WARNING:

(env) $ juju deploy ./tutocharm.charm
WARNING tutocharm does not declare supported series in metadata.yml
Deploying charm "local:bionic/tutocharm-0".

And in the juju debug-log:

unit-tutocharm-0: 19:06:32 INFO unit.tutocharm/0.juju-log Congratulations, the charm was properly installed!                                                                                   

Anyway the charm was deployed:

(env) $ juju status
Model    Controller  Cloud/Region         Version  SLA          Timestamp
default  overlord    localhost/localhost  2.8.5    unsupported  19:17:27-03:00

App         Version  Status   Scale  Charm       Store       Rev  OS      Notes
hello-juju           error        1  hello-juju  jujucharms    4  ubuntu
postgresql  10.14    active       1  postgresql  jujucharms  208  ubuntu
tutocharm            unknown      1  tutocharm   local         0  ubuntu

Unit           Workload  Agent  Machine  Public address  Ports     Message
hello-juju/0*  error     idle   0    80/tcp    hook failed: "db-relation-changed"
postgresql/0*  active    idle   1    5432/tcp  Live master (10.14)
tutocharm/0*   unknown   idle   2

Machine  State    DNS           Inst id        Series  AZ  Message
0        started  juju-396e11-0  bionic      Running
1        started  juju-396e11-1  bionic      Running
2        started  juju-396e11-2  bionic      Running

@jose Glad you liked it!

We still need to add some verifications in charcraft to “discover” any possible problems in the charm being built.

This is tracked in this issue.

1 Like