[draft] Operator Charm Writing Guide

Charms in detail

Table of contents

Best practices

Charm writing in detail

New to charms?

Charm Writing

Example Charms

Best practices

Repository Naming

Naming follows the convention of charm-<charm_name> for example charm-percona-cluster.

The rationale behind this is quick and easy searching of the 1000s of github projects along with other reasons.

New to charms?

This covers aspects of charming which will be useful when creating them

Why charms?

The objective of charms is the following: take the complication of managing 100s potentially 1000s of configuration files and abstract them as python modules. Thus by abstraction creating a true insfrastructure as code.

One of the ways that charms achieves this goal is through what are called relations, you can think about this much like the Database relation charts you might have seen in previous roles, or at university, the concept is much the same.

Here’s an example from the Juju GUI:

What you are looking at here is a Kubernetes, rendered, using Juju and charms. Each of the circles represents an application, each of the lines represents what we call a relation.

More info and history on charms can be found here.

Charm writing

The operator framework has a tool which you can use to create a charm for you, to install the charm tools firstly install the snap:

sudo snap install charm

You can now create the charm:

charm create -t operator-python mynewcharm

Before we get start lets go over some of the concepts of charming.

How do relations work?

Relations between charms at a high level work through what are called interfaces, interfaces are the glue that sticks two or more charms together and allows them to communicate.

Some history

In the old framework (reactive) charms were found on github, and used the naming convention of interface-<interface-name>. This stays with the new framework.

The major change is the removal of requires.py and provides.py these are now replaced by a simplied interface_<interface_name>.py.

Here’s the picture you might have in when you think of charm relations:

conception of charm relations

The problem with this conception is what if want to create a new charm to use somecharm:relation, well then not only do I have to create a new charm, and program the relation into it, I also have to adapt my original charm to work for my new charm. The two components are too closely coupled.

So the charm framework deals with this by decoupling the relations, this is where we move onto the next section.

Charm writing

TODO: Change this for the new charm generation tools

To start writing charms you will need to setup a development environment with git and a skeleton charm.

mkdir mynewcharm
cd $_

Included in the docs directory of this repository is a set of tools, we’re going to use the letscharm script to initialise a set of charms for our use.


This tool will ask you whether you are creating a K8S charm or a IAAS charm. Pick the correct one as appropriate.

Add the operator framework

To begin a new charm, you will need to import the operator framework. The charm operator framework is imported by adding the framework in the ‘lib’ diretory and as a submodule:

git submodule add https://github.com/canonical/operator mod/operator
ln -s ../mod/operator/ops lib/ops

Add interface dependences

To begin a new charm we should take a note of any interfaces that might be needed, the operator framework uses interfaces as dependencies.

These dependencies are pulled in as git submodules, and should be added to the .gitmodules file, an example file looks like this:

[submodule "mod/operator"]
	path = mod/operator
	url = https://github.com/canonical/operator
[submodule "mod/interface-mysql"]
	path = mod/interface-mysql
	url = git@github.com:johnsca/interface-mysql.git
[submodule "mod/interface-http"]
	path = mod/interface-http
	url = git@github.com:johnsca/interface-http.git
[submodule "mod/resource-oci-image"]
	path = mod/resource-oci-image
	url = git@github.com:johnsca/resource-oci-image.git

You can then pull in those dependencies with the following commands:

git submodule init
git submodule update

These commands will pull in the dependencies to mod, although we will be referencing from the lib directory. To fix this you will need to create symbolic references.

ln -s ./mod/interface-mysql/interface_mysql.py ./lib/interface_mysql.py

This is requied for all of the required submodules.

You can learn more about writing Interfaces here


The operator charms are invoked as scripts on the running machine. As such you will be required to add a shbang at the top of the python file:

#!/usr/bin/env python3

After this, you will then need to adapt the system path to import the framework:

import sys

Once this is done, you can then move on to import the Framework classes

from ops.charm import CharmBase
from ops.main import main
from ops.framework import StoredState
from oci_image import OCIImageResource

Followed by any other imports, for example, interfaces:

from interface_mysql import MySQLClient

Creating the charm class

All Operator Framework charms should inherit from the CharmBase class. This gives them access to the internals

For example:

class MyCharm(CharmBase):

The charm __init__ method

The charm init method has the following signature:

def __init__(self, framework, key)
             ^^^^  ^^^^^^^^^  ^^^
             ||||  |||||||||  ||||
 1. Reference to the class instance
                   |||||||||  ||||
 2.      A reference to the framework
 3.                           key (described below)

Number 3. Is yet unused in the framework.

Once you have the above __init__ signature. You will need to call super:

    def __init__(self, framework, key):
        super.__init__(framework, key)

You can then follow up the rest of the method with calls to set our state and our required interfaces for example:

        self.mysql = MySQLClient(self, 'mysql')

The charm model

This is the central place to get all relevant charm information so that you can configure your charm.

The charm model has the following properties:

  • unit
  • app
  • relations - A relation mapping
  • config - The charm configuration
  • resources
  • pod - the Kubernetes pod
  • storages - The charm storage - (TODO)

Charm metadata is retrieved from the meta framework attribute:

meta = self.framework.meta

The same can be done for the charm configuration data too:

config = self.framework.config

Hooking into events

In the __init__ call you can setup events to be called, for example when the database is available:


A popular event to track in the framework is the config_changed event:

	self.framework.observe(self.on.config_changed, self.configure_pod)

To get a list of events for an interface you will need to check the docs of that interface. For framework events see here

Your event listener methods should accept the event as an arguement. The required signature is below:

def my_event_listener(self, event):

You are able to add additional params if you so wish but they must be optional, otherwise a TypeError will be thrown.

Deferring events

In the new framework events can now be deferred. When an event is deferred:

  • Parameters will be saved when events are deferred;
  • Parameters will be restored when events are reemitted;

To defer an event, all you need to is called defer(). An example of this can be found below:

def on_db_peer_relation_changed(self, event):
    if not self.state.ready:

NOTE: ActionEvents cannot be deferred. Doing so will trigger a RuntimeError.

Subordinate Charms

Charm Actions


Migrating from the old (reactive) framework

For the most part and charm code you write in the old framework may more or less be portable to the new Framework.


Please find the testing guide here

Useful links


Example Charms

Gitlab charm

Cockroachdb (Example Charm)

Charm Kine

Test Charm (From the Operator Framework)


1 Like

@cory_fu is this material that is up-to-date or has this been surpassed by something else?