The Headache of Managing Multiple Services at Once
When a production system has around 5–10 services running in parallel — MySQL, Redis, Nginx, a Flask app, Celery workers — manual management starts becoming a problem with no clean solution. I used to maintain a pile of bash scripts named deploy.sh in every variation imaginable. Every time a new service was added, I had to write another script, update documentation (which nobody ever read), and remember the correct startup order: “MySQL must come up first, then Redis, then the app…”.
Then came the need to scale: a client needed 2 more web nodes to handle peak traffic. Install each one manually, copy configs, test connections… Half a day gone. Not to mention the anxiety every update brought — never knowing which service would break. New team members had to wade through a dozen wiki pages to understand the system, and the wiki was always at least a few months behind reality.
Root Cause Analysis: What Are Traditional Tools Missing?
Ansible, Chef, Puppet — all solid tools for configuration management. But they share the same blind spot: they only describe “WHAT” (how to configure a machine) without understanding “HOW” (how services relate to each other).
With Ansible, you write playbooks to install MySQL on node A and WordPress on node B. But the relationship between them — which user WordPress uses to connect to MySQL, which database, which password — has to be hardcoded or managed separately with variables. When the topology changes (adding a MySQL replica, changing an IP), you have to manually update 3–4 different places before re-running the playbook.
Kubernetes + Helm handles container orchestration well. But if you’re running on bare metal, older VMs, or have services that can’t be easily containerized — legacy databases, hardware-specific workloads — Helm isn’t the right fit.
What all three miss: none of them understand the semantics of service relationships — “WordPress needs a database,” “Kafka needs ZooKeeper,” “Grafana needs Prometheus as a datasource.” When the topology changes, humans still have to step in and intervene manually.
Approaches to Complex Orchestration Problems
Option 1: Version-controlled manual scripts
Writing careful bash/Python scripts, putting them in Git, and fully documenting them. This is the most common approach, but it’s expensive to maintain and hard to scale — every topology change requires manual intervention.
Option 2: Ansible with dynamic inventory
Using Ansible dynamic inventory combined with group_vars to manage service relationships. Better than raw scripts, but you still have to write your own logic for handling services being added or removed.
Option 3: Kubernetes Operators
If you’re already on Kubernetes, the Operators pattern (like what the Prometheus Operator or MongoDB Operator does) solves the lifecycle management problem in a container environment. However, it requires a K8s cluster and deep knowledge of Go/Python to write your own operator — typically taking weeks before you’re confident enough to call it production-ready.
Option 4: Juju with Charmed Operators
Juju brings the operator pattern to bare metal and VMs, not just Kubernetes. Charmed Operators (Charms) are Python packages that encapsulate all the logic for deploying, configuring, scaling, and upgrading — including the relationships with other services.
The Best Approach: Juju Model-driven Orchestration
Core concepts to understand
- Controller: The “brain” of Juju — manages the entire system, runs on a dedicated machine or container
- Model: A logical grouping of applications and their relationships (similar to a namespace)
- Charm: A Python package containing the deploy/configure/scale/upgrade logic for a specific application
- Relation: Defines how two services exchange information — credentials, endpoints, config
- Unit: A single instance of an application (similar to a Pod in K8s)
Installing Juju on Ubuntu
sudo snap install juju --classic
Bootstrapping the controller with LXD (local environment)
# Install LXD if not already installed
sudo snap install lxd
lxd init --auto
# Bootstrap controller — Juju creates a dedicated LXD container as the controller
juju bootstrap localhost lxd-controller
On the company’s staging environment running Ubuntu 22.04, I tested this config before pushing to production. Bootstrapping takes about 3–5 minutes depending on network speed.
Deploying applications and connecting services
# Create a new model
juju add-model mywebapp
# Deploy MySQL and WordPress from Charmhub
juju deploy mysql
juju deploy wordpress
# Monitor status — wait for "active"
juju status
Typical output of juju status:
App Version Status Scale Charm
mysql 8.0.36 active 1 mysql
wordpress 6.4.2 waiting 1 wordpress waiting: ready
Unit Workload Agent Public address
mysql/0* active idle 10.0.0.10
wordpress/0* waiting idle 10.0.0.11
The most important step — connecting the two services:
juju integrate wordpress:db mysql:db
After this command, Juju automatically:
- MySQL charm creates a database and new user with random credentials
- Passes host, port, dbname, user, and password to the WordPress charm via relation data
- WordPress charm writes
wp-config.phpwith that information - Restarts PHP-FPM if needed — no SSH required
Lifecycle management: scale, config, upgrade
# Scale WordPress to 3 units — Juju automatically creates containers, deploys, and connects to MySQL
juju scale-application wordpress 3
# View and set config — charm applies automatically, no SSH needed
juju config wordpress
juju config wordpress blog-title="IT From Zero"
# Upgrade charm to a new version
juju refresh wordpress --channel latest/stable
# Run actions available in the charm
juju actions mysql # List available actions
juju run mysql/0 backup # Run backup
Deploying a complex stack: Kafka + monitoring in minutes
juju add-model kafka-stack
# Deploy Kafka and ZooKeeper
juju deploy kafka --channel 3/stable
juju deploy zookeeper --channel 3/stable
juju integrate kafka:zookeeper zookeeper:zookeeper
# Add monitoring — Prometheus + Grafana configure each other automatically
juju deploy prometheus2
juju deploy grafana
juju integrate kafka:metrics prometheus2:scrape
juju integrate prometheus2:grafana-source grafana:grafana-source
# View the full system
juju status --color
The result: a Kafka cluster with Prometheus metrics and a fully configured Grafana dashboard — without writing a single line of manual config.
Practical notes when adopting Juju
- Juju doesn’t replace Docker/Kubernetes — it complements them. Juju can deploy charms onto a K8s cluster (Juju + K8s mode), where charms manage Kubernetes resources instead of virtual machines.
- Choose quality charms: not every charm on Charmhub is production-ready. Prioritize charms with a Canonical badge or high download counts, and read the README carefully before using them in production.
- Invest time learning the concepts: spend 1–2 days understanding models, controllers, charms, relations, and units before deploying to production.
- Best suited for complex stacks: Charmed Kubernetes, MLOps with the Kubeflow charm, data platforms with Spark + HDFS. For 1–2 simple services, Ansible is still the faster choice.
In short: Juju doesn’t solve the problem of “installing software on a machine” — it solves the problem of “managing the full lifecycle of complex applications in a way that’s reproducible and standardized for the entire team.” That’s something Ansible, Chef, and Helm were never designed to do.

