Here is a simple, hands-on Juju project using the modern Ops Framework called The Pizzeria Deployment.
We will build two charms:
pizza-shop(The frontend/order manager)pizza-oven(The backend baker)
This project will teach you how to handle events, read configs (like setting the daily special), and connect services using juju relations (sending pizza orders to the oven).
[ pizza-shop ] <--- (Config: daily-special="Pepperoni")
|
| Relation: pizza-order
v
[ pizza-oven ] <--- (Fires event: order-received)
We will create a relation interface called pizza-order. The pizza-shop will send the requested pizza type across the relation, and the pizza-oven will log that it is baking it.
This charm will take a configuration option for the "daily special" and pass that order to any connected oven.
type: charm
name: pizza-shop
summary: Manages customer pizza orders.
description: A learning charm to simulate a pizza shop front.
bases:
- build-on:
- name: ubuntu
channel: "22.04"
run-on:
- name: ubuntu
channel: "22.04"
# 1. Defining the Configuration
config:
options:
daily-special:
type: string
default: "Margherita"
description: "The pizza flavor of the day."
# 2. Defining the Relation
provides:
pizza-order:
interface: pizza-delivery
#!/usr/bin/env python3
import logging
from ops.charm import CharmBase
from ops.main import main
from ops.model import ActiveStatus
logger = logging.getLogger(__name__)
class PizzaShopCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
# Observe events
self.framework.observe(self.on.install, self._on_install)
self.framework.observe(self.on.config_changed, self._on_config_changed)
self.framework.observe(self.on.pizza_order_relation_joined, self._on_pizza_order_relation_joined)
def _on_install(self, event):
self.unit.status = ActiveStatus("Shop opened!")
def _on_config_changed(self, event):
# Read config value
special = self.config.get("daily-special")
logger.info(f"Daily special updated to: {special}")
# If we are already related to an oven, update the order dynamically
if self.model.relations['pizza-order']:
for relation in self.model.relations['pizza-order']:
relation.data[self.unit]["requested-pizza"] = special
self.unit.status = ActiveStatus(f"Serving {special}")
def _on_pizza_order_relation_joined(self, event):
# Grab the current config and send it across the relation bucket
special = self.config.get("daily-special")
event.relation.data[self.unit]["requested-pizza"] = special
logger.info(f"Sent order for {special} to the oven!")
if __name__ == "__main__":
main(PizzaShopCharm)This charm listens for incoming relations from a shop and processes the data sent over the wire.
type: charm
name: pizza-oven
summary: Bakes the pizzas sent from the shop.
description: A learning charm simulating a backend baking service.
bases:
- build-on:
- name: ubuntu
channel: "22.04"
run-on:
- name: ubuntu
channel: "22.04"
# Consumes the relation provided by the shop
requires:
pizza-order:
interface: pizza-delivery
#!/usr/bin/env python3
import logging
from ops.charm import CharmBase
from ops.main import main
from ops.model import ActiveStatus, WaitingStatus
logger = logging.getLogger(__name__)
class PizzaOvenCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
self.framework.observe(self.on.install, self._on_install)
# Observe the relation-changed event to know when data updates
self.framework.observe(self.on.pizza_order_relation_changed, self._on_pizza_order_changed)
def _on_install(self, event):
self.unit.status = WaitingStatus("Oven is cold. Waiting for orders...")
def _on_pizza_order_changed(self, event):
# Read data provided by the remote unit (the shop)
remote_unit = event.unit
pizza_type = event.relation.data[remote_unit].get("requested-pizza")
if pizza_type:
logger.info(f"🔥 [OVEN] Baking a delicious {pizza_type} pizza! 🔥")
self.unit.status = ActiveStatus(f"Baking {pizza_type}")
else:
self.unit.status = WaitingStatus("Connected to shop, but no pizza requested yet.")
if __name__ == "__main__":
main(PizzaOvenCharm)- Ops Framework Events: Every method we registered via
self.framework.observe(self.on.<event>, self.<method>)is a Juju lifecycle hook. - Configs:
self.config.get("key")pulls user-defined runtime values directly into your charm execution loop. - Relations (
relation-joinedvsrelation-changed): * When the shop joins, it triggersrelation-joinedand drops data into its local relation data bucket (relation.data[self.unit]). - When that data bucket is populated, it triggers
relation-changedon the oven's side, allowing the oven to read the data usingevent.relation.data[remote_unit].
If you want to pack and test this locally in a microk8s or LXD controller environment:
- Pack both charms:
Run
charmcraft packinside both project directories to generate the.charmfiles. - Deploy them:
juju deploy ./pizza-shop_ubuntu-22.04-amd64.charm
juju deploy ./pizza-oven_ubuntu-22.04-amd64.charm
- Relate them:
juju relate pizza-shop:pizza-order pizza-oven:pizza-order
- Watch the logs & change config:
Open a separate terminal and run
juju debug-log. Then dynamically change the order:
juju config pizza-shop daily-special="Pepperoni"
You will instantly see the config_changed event fire on the shop, which updates the relation data, triggering relation_changed on the oven to start baking Pepperoni!
Would you like to expand this by adding a custom Juju Action (e.g., a clear-oven or serve-order command)?