Many applications require access to some persistent storage. The Juju OLM and Charmed Operator Framework provide a facility for working with and defining charms that require storage.

Defining storage

Charm storage is defined in metadata.yaml. As a reminder, you can see the full specification for this on Discourse. Storage is defined as a top-level YAML map in metadata.yaml, keys for the map represent the name of the storage volume.

The storage map definition:

Field Type Default Description
type string required Type of storage requested. Supported values are block or filesystem.

The filesystem type yields a directory in which the charm may store files. The block type yields a raw block device, typically disks or logical volumes.

If the charm specifies a filesystem-type store, and the storage provider supports provisioning only disks, then a disk will be created, attached, partitioned, and a filesystem created on top. The filesystem will be presented to the charm as normal.
description string nil Description of the storage requested
multiple map
(see table below)
nil By default, stores are singletons; a charm will have exactly one of the specified stores. The multiple field specifies the number of storage instances to be requested.

Unless a number is explicitly specified during deployment, units of the application will be allocated the minimum number of storage instances specified in the charm metadata. It is then possible to add instances (up to the maximum) by using the juju storage add command.
minimum-size string 1GiB Size in the forms: 1.0G, 1GiB, 1.0GB. Supported size multipliers are M, G, T, P, E, Z, Y. Not specifying a multiplier implies M.
location string nil Specifies the mount location for filesystem stores. For multi-stores, the location acts as the parent directory for each mounted store.
properties string[] nil List of properties for the storage. Currently only transient is supported

The multiple map definition:

Field Type Default Description
range string/int nil Value can be an int for a precise number, or a string in the forms: m-n, m+, m-, where m and n are of type int.

Examples: range: 2 or range: 0-10.

An example of a storage definition inside metadata.yaml:

# ...
  # Name of this storage is 'data'
    type: filesystem
    description: junk storage
    minimum-size: 100M
    location: /srv/data
# ...

Storage on Kubernetes

In addition to the above, there is some additional data required to define storage for Kubernetes charms. You will still need to define the top-level storage map (as above), but also specify which containers you would like the storage mounted into. Consider the following metadata.yaml snippet:

# ...
  # define a container named "important-app"
    # use the "app-image" oci resource
    resource: app-image
    # mount our 'logs' store at /var/log/important-app
    # in the container
      - storage: logs
        location: /var/log/important-app
  # This is another container with no storage
    resource: supporting-app-image

    type: filesystem
# ...

The above snippet will ensure that both the important-app container and charm container inside each Pod has the logs store mounted. Under the hood, the storage map is translated into a series of PersistentVolumes, mounted into Pods with PersistentVolumeClaims.

The location attribute must be specified when mounting a storage into a workload container as shown above - this will dictate the mount point for the specific container.

Optionally, developers can specify the location attribute on the storage itself, which will specify the mount point in the charm container. If left unset, the charm container will have the storage volume mounted at a predictable path at /var/lib/juju/storage/<name>/<num>, where <num> is the index of the storage. This defaults to 0.

For the above metadata.yaml, the charm container would have the storage available at: /var/lib/juju/storage/logs/0.

Storage events

There are two key events associated with storage:

Event name Event Type Description
<name>_storage_attached StorageAttachedEvents This event is triggered when new storage is available for the charm to use. Callback methods bound to this event allow the charm to run code when storage has been added.

Such methods will be run before the install event fires, so that the installation routine may use the storage. The name prefix of this hook will depend on the storage key defined in the metadata.yaml file.
<name>_storage_detaching StorageDetachingEvent Callback methods bound to this event allow the charm to run code before storage is removed.

Such methods will be run before storage is detached, and always before the stop event fires, thereby allowing the charm to gracefully release resources before they are removed and before the unit terminates.

The name prefix of the hook will depend on the storage key defined in the metadata.yaml file.

Storage model

Developers have access to a model of the available storage associated with the Juju model that the charm is deployed to. This is accessed through self.model.storages in the context of a charm, which returns a mapping of <storage_name> to Storage objects, which exposes the name, id and location of each storage to the charm developer, where id is the underlying storage provider ID.

The StorageMapping returned by self.model.storages also exposes a request method (e.g. self.model.storages.request()) which provides an expedient method for the developer to invoke the underlying storage-add hook tool in the charm to request additional storage. On success, this will fire a <storage_name>-storage-attached event.