From Jupyter Notebooks to Operational Runbooks
Jupyter notebooks are fantastic for data science. But if you’re using them for operational runbooks, there’s a better way.
This guide helps you understand when Jupyter fits and when markdown runbooks are the better choice. For tool comparisons, see our interactive runbook tools comparison.
Why Teams Start with Jupyter
Jupyter’s appeal for ops is understandable:
- Execution built in: Click to run cells
- Output captured: See results inline
- State preserved: Variables persist between cells
- Familiar: Many engineers have used it
A typical ops notebook looks like:
# Cell 1: Setup
import subprocess
import json
def run(cmd):
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
print(result.stdout)
if result.returncode != 0:
print(f"Error: {result.stderr}")
return result
# Cell 2: Check pods
run("kubectl get pods")
# Cell 3: Check logs
run("kubectl logs -l app=api --tail=50")
It works. But there are hidden costs.
The Hidden Costs of Jupyter for Ops
Cost 1: The Python Wrapper Tax
Every shell command needs a Python wrapper:
# What you want to run
kubectl get pods
# What you actually write
import subprocess
result = subprocess.run(["kubectl", "get", "pods"], capture_output=True, text=True)
print(result.stdout)
This adds:
- Boilerplate code
- Escaping complexity
- Debugging friction
Cost 2: Environment Complexity
To run a Jupyter notebook:
# Requirements
python3 -m venv venv
source venv/bin/activate
pip install jupyter
pip install your-dependencies
# Start
jupyter notebook
During an incident, this setup time matters.
Cost 3: The JSON Problem
Notebook files are JSON:
{
"cells": [{
"cell_type": "code",
"source": ["run(\"kubectl get pods\")"]
}]
}
This means:
- Git diffs are noisy
- Merge conflicts are painful
- Raw files are unreadable
Cost 4: Output Bloat
Notebooks store outputs:
{
"outputs": [{
"output_type": "stream",
"text": ["NAME READY STATUS RESTARTS AGE\n",
"api-7d4f8b6c9-x2k4j 1/1 Running 0 2d\n",
"... 50 more lines ..."]
}]
}
Your repo fills with stale command output.
Cost 5: Collaboration Challenges
Two people edit the same notebook:
<<<<<<< HEAD
"execution_count": 15,
=======
"execution_count": 23,
>>>>>>> feature-branch
Merge conflicts on execution counts, outputs, and metadata.
When Jupyter Is Right
Keep Jupyter for:
| Use Case | Why Jupyter Fits |
|---|---|
| Data analysis | Rich visualizations, dataframes |
| Machine learning | Model training, metrics tracking |
| API exploration | Interactive testing with state |
| Prototyping | Quick iteration with outputs |
When Runbooks Are Better
Switch to markdown runbooks for:
| Use Case | Why Markdown Fits |
|---|---|
| Incident response | Readability, no setup time |
| Standard procedures | Git-friendly, reviewable |
| Team documentation | Universal access |
| Automated execution | Clean parsing |
Migration Path
Step 1: Identify Runbook Candidates
Look for notebooks that are:
- Primarily shell commands
- Used by multiple team members
- Referenced during incidents
- Mostly linear execution
Step 2: Extract the Commands
From this notebook:
# Check service health
run("kubectl get pods -l app=api")
# Check recent logs
run("kubectl logs -l app=api --tail=100 | grep -i error")
# Restart if needed
run("kubectl rollout restart deployment/api")
To this markdown:
# API Service Health Check
## Check service health
```bash
kubectl get pods -l app=api
```
## Check recent logs
```bash
kubectl logs -l app=api --tail=100 | grep -i error
```
## Restart if needed
```bash
kubectl rollout restart deployment/api
```
Step 3: Add Context
Notebooks often lack context because the author “just knew”:
# API Service Health Check
Use this runbook when the API health alert fires.
## Prerequisites
- kubectl configured for production
- Access to production namespace
## Check service health
First, verify pods are running:
```bash
kubectl get pods -l app=api
```
**Healthy**: All pods show Running, READY 1/1
**Unhealthy**: Pods in CrashLoopBackOff or not Ready
## Check recent logs
Look for error patterns:
```bash
kubectl logs -l app=api --tail=100 | grep -i error
```
**If you see** "connection refused": Database may be down
**If you see** "OOM killed": Memory limits too low
## Restart if needed
Only restart after identifying the issue:
```bash
kubectl rollout restart deployment/api
```
Wait 60 seconds, then verify with the health check above.
Step 4: Choose Execution Method
Options for making markdown executable:
| Method | Tradeoff |
|---|---|
| Copy-paste | Universal but manual |
| Runme (VS Code) | Easy but requires VS Code |
| Stew | Web + terminal, built for this |
Keeping What Works
You can keep Jupyter for what it’s good at while using markdown runbooks for ops:
docs/
├── runbooks/ # Markdown runbooks
│ ├── api-health.md
│ └── db-recovery.md
├── notebooks/ # Jupyter for analysis
│ ├── metrics-analysis.ipynb
│ └── incident-review.ipynb
Conversion Script
Automate simple conversions:
#!/usr/bin/env python3
"""Convert Jupyter notebook shell commands to markdown runbook."""
import json
import sys
import re
def convert(notebook_path):
with open(notebook_path) as f:
nb = json.load(f)
output = []
for cell in nb['cells']:
if cell['cell_type'] == 'markdown':
output.append(''.join(cell['source']))
output.append('')
elif cell['cell_type'] == 'code':
source = ''.join(cell['source'])
# Extract shell commands from run() calls
matches = re.findall(r'run\(["\'](.+?)["\']\)', source)
for cmd in matches:
output.append('```bash')
output.append(cmd)
output.append('```')
output.append('')
return '\n'.join(output)
if __name__ == '__main__':
print(convert(sys.argv[1]))
Stew: Built for Runbooks
Stew is purpose-built for operational runbooks:
- Markdown-native: No format conversion needed
- Shell-first: Bash commands run directly
- Git-friendly: Clean diffs, easy reviews
- Team-ready: Share without environment setup
Your team gets execution without Jupyter’s overhead.
Join the waitlist and simplify your operational docs.