Skip to content
academia.sh

Lesson 01 / 12

What Is DevOps?

The technical and organizational side of shared responsibility: in the same 22 occurrences' separate observation scopes, 9 are visible only to operations, 1 only to development, and 1 in neither scope; 4 occurrences never reach their owner and reaching one averages 7.0 steps; sharing three surfaces brings the unreached count to 1 and the reach time to 5.0 steps; fully combining the scopes brings reach down to 1.6 steps while the development team's load rises from 95 to 107 steps, lead time rises from 25.8 to 34.8 steps, and deployment frequency falls from 5 to 4 releases.

Contents

The previous lesson measured handover as a contract: twenty of the twenty-eight responsibility items were handed over, eight stayed in development for lack of information, tooling, or procedure. The contract wrote down who would do what. What it did not write down was who would see what. Two roles can be jointly responsible for the same service and still see the same occurrence at a different step, on a different surface, sometimes not at all.

Shared responsibility holds the development and operations roles jointly accountable for a service’s outcome. A handover contract divides items; shared responsibility divides the outcome, and the second requires, as a precondition, that both roles can see the same occurrence. This lesson measures that precondition: the same set of occurrences is dropped into two roles’ observation scope, how many occurrences stay visible to only one side is counted, and the bill for combining scope is worked out.

Observation Scope Is a Data Structure

The example is this curriculum’s fictional regional measurement network: software that collects readings from a municipality’s water meters, verifies the readings, turns them into invoices, and opens work orders for field crews. The network and the teams are both fictional. CF1: there are three environments (development, test, production), two teams (development and operations), and one nightly batch job. CF2: there are eight observation surfaces; three sit in the development team’s scope, four in the operations team’s scope, and invoice-statement sits in no one’s scope. CF3: twenty-two occurrences are born within one window; each occurrence has a class (error, delay, drift), a birth step, and the surfaces it appears on together with its delay on each surface. CF4: every occurrence has an owner who can close it — development, operations, or shared; a shared occurrence cannot be closed until it is visible in both scopes, and closing takes 4, 2, and 5 steps respectively. CF5: an occurrence visible in a scope writes 2 steps of triage for that role; an occurrence that has not reached its owner within 30 steps is surfaced from above, and because the context has to be rebuilt secondhand this adds 2 extra steps. CF6: the development team works toward a release during its idle steps, and a release needs 8 steps of work; every occurrence attaches to the last release before it, and a release with an attached occurrence that never reaches its owner counts as failed. The window is 140 steps.

The occurrences, the surfaces, and the two roles’ rotation are a process-internal model; there is no real alert stream or incident log.

// observation/scope.mjs — the fictional regional measurement network's occurrence and observation-scope
// model (model): there is no real alert stream or incident log, steps are a data structure. No randomness.

export const WINDOW = 140;                   // measured step window
export const TRIAGE = 2;                     // cost to one role of triaging an occurrence (steps)
export const RELEASE_WORK = 8;               // work steps a release needs
export const CLOSURE = { dev: 4, ops: 2, shared: 5 };
export const CEILING = 30;                   // an occurrence that never reaches its owner surfaces at this delay
export const SECOND_HAND = 2;                // extra context cost of an occurrence surfaced this way

export const SURFACE = {                     // surface -> whose scope it sits in, in the separate arrangement
  "app-record": "dev", "error-tracking": "dev", "release-log": "dev",
  "work-schedule": "ops", "machine-metrics": "ops", "alert-stream": "ops",
  "support-record": "ops", "invoice-statement": "-",
};
export const ROLE = ["dev", "ops"];

const table = (s, f) => s.trim().split("\n").map((r) => f(r.trim().split(/\s+/)));

// occurrence: birth step, class, role that can close it, name, surfaces it appears on (surface:delay)
export const OCCURRENCE = table(`
    2  error  dev    reading-parser            app-record:0 support-record:12
    6  delay  ops    batch-overflow            work-schedule:1 machine-metrics:2
   11  drift  dev    untested-setting          release-log:0 alert-stream:9
   17  error  ops    queue-jam                 machine-metrics:0 alert-stream:1
   21  delay  dev    query-slowdown            app-record:3 machine-metrics:1
   26  error  dev    duplicate-invoice         support-record:16
   31  drift  ops    disk-full                 machine-metrics:0
   36  error  dev    blank-address-work-order  error-tracking:2 support-record:9
   42  delay  ops    network-latency           machine-metrics:1 alert-stream:2
   46  drift  dev    library-drift             release-log:1
   51  error  shared nightly-job-incomplete    work-schedule:1 app-record:4
   57  error  dev    negative-consumption      invoice-statement:0 support-record:14
   61  delay  ops    backup-collision          work-schedule:2 machine-metrics:3
   66  drift  shared timezone-difference       app-record:6 work-schedule:3
   71  error  dev    rounding-difference       invoice-statement:0
   77  delay  dev    cache-warmup              error-tracking:2 machine-metrics:1
   81  error  ops    connection-pool           alert-stream:1 app-record:0
   86  drift  dev    missing-env-var           app-record:0 alert-stream:5
   91  error  dev    meter-id-conflict         error-tracking:2 support-record:10
   96  delay  ops    job-rerun                 work-schedule:1
  102  drift  dev    format-break              invoice-statement:0 support-record:15
  107  error  shared duplicate-work-order      app-record:3 support-record:8`,
([born, cls, owner, name, ...y]) => ({ born: +born, cls, owner, name,
  surface: y.map((p) => ({ name: p.split(":")[0], delay: +p.split(":")[1] })) }));

// Arrangement: on top of the separate arrangement, which role additionally sees which surface (contrast run).
export const ARRANGEMENT = {
  separate: { dev: [], ops: [] },
  selected: { dev: ["support-record"], ops: ["release-log", "invoice-statement"] },
  combined: { dev: Object.keys(SURFACE), ops: Object.keys(SURFACE) },
};

export const scope = (d) => Object.fromEntries(ROLE.map((r) =>
  [r, new Set([...Object.keys(SURFACE).filter((y) => SURFACE[y] === r), ...ARRANGEMENT[d][r]])]));

export function run(d) {
  const S = scope(d);
  const G = OCCURRENCE.map((o) => {              // in which step each occurrence is seen by each role
    const g = Object.fromEntries(ROLE.map((r) => {
      const v = o.surface.filter((y) => S[r].has(y.name)).map((y) => y.delay);
      return [r, v.length ? Math.min(...v) : null];
    }));
    const closeStep = o.owner === "shared"        // a shared occurrence must be seen in both scopes
      ? (g.dev === null || g.ops === null ? null : Math.max(g.dev, g.ops))
      : g[o.owner];
    return { o, g, closeStep, ownerRole: o.owner === "shared" ? "dev" : o.owner };
  });

  const queue = Object.fromEntries(ROLE.map((r) => [r, []]));
  const remaining = Object.fromEntries(ROLE.map((r) => [r, 0]));
  const load = Object.fromEntries(ROLE.map((r) => [r, 0]));
  const closure = new Map(), releases = [];
  let accumulated = 0, start = 0;

  for (let t = 0; t <= WINDOW; t++) {
    for (const x of G) {
      for (const r of ROLE)                       // an occurrence seen in scope opens triage work
        if (x.g[r] !== null && t === x.o.born + x.g[r]) queue[r].push([TRIAGE, null]);
      if (x.closeStep !== null && t === x.o.born + x.closeStep)
        queue[x.ownerRole].push([CLOSURE[x.o.owner], x]);
      else if (x.closeStep === null && t === x.o.born + CEILING)   // surfaced from above
        queue[x.ownerRole].push([CLOSURE[x.o.owner] + SECOND_HAND, x]);
    }
    for (const r of ROLE) {
      if (remaining[r] === 0 && queue[r].length) [remaining[r]] = queue[r][0];
      if (remaining[r] > 0) {                      // role is busy with an occurrence this step
        remaining[r]--; load[r]++;
        if (remaining[r] === 0) { const [, x] = queue[r].shift(); if (x) closure.set(x.o.name, t + 1); }
        continue;
      }
      if (r !== "dev") continue;                   // idle step: the dev team works toward a release
      if (accumulated === 0) start = t;
      if (++accumulated === RELEASE_WORK) { releases.push({ t: t + 1, start, occurrence: [] }); accumulated = 0; }
    }
  }
  for (const x of G) {                             // an occurrence attaches to the release before it
    const y = [...releases].reverse().find((v) => v.t <= x.o.born);
    if (y) y.occurrence.push(x);
  }
  return { G, load, closure, releases };
}

export const avg = (a) => (a.length ? (a.reduce((s, v) => s + v, 0) / a.length).toFixed(1) : "-");

export function measure(d) {
  const R = run(d), count = (f) => R.G.filter(f).length;
  const reach = R.G.map((x) => (x.closeStep === null ? CEILING : x.closeStep));   // ceiling for those never reached
  const duration = R.G.map((x) => (R.closure.get(x.o.name) ?? WINDOW) - x.o.born);
  const broken = R.releases.filter((y) => y.occurrence.some((x) => x.closeStep === null)).length;
  return { R,
    devOnly: count((x) => x.g.dev !== null && x.g.ops === null),
    opsOnly: count((x) => x.g.ops !== null && x.g.dev === null),
    both: count((x) => x.g.dev !== null && x.g.ops !== null),
    neither: count((x) => x.g.dev === null && x.g.ops === null),
    unreached: count((x) => x.closeStep === null), reachStep: avg(reach),
    noiseDev: count((x) => x.g.dev !== null && x.o.owner === "ops"),
    noiseOps: count((x) => x.g.ops !== null && x.o.owner === "dev"),
    loadDev: R.load.dev, loadOps: R.load.ops, recovery: avg(duration),
    releaseCount: R.releases.length, leadTime: avg(R.releases.map((y) => y.t - y.start)),
    changeFailureRate: R.releases.length ? (100 * broken / R.releases.length).toFixed(0) + "%" : "-" };
}

The four delivery metrics are read from the same run: deployment frequency is the number of releases in the window, lead time is the steps from when work starts on a release to when it goes out, change failure rate is the share of failed releases, time to restore is the steps from an occurrence’s birth to its closure.

// observation/measure.mjs — how an occurrence falls into the two roles' observation scope, and what combining the scopes costs.
import { OCCURRENCE, SURFACE, ROLE, measure, scope, CEILING } from "./scope.mjs";

const print = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const D = ["separate", "selected", "combined"];
const R = Object.fromEntries(D.map((d) => [d, measure(d)]));

console.log(`${OCCURRENCE.length} occurrences, ${Object.keys(SURFACE).length} observation surfaces; ` +
  ["error", "delay", "drift"].map((s) =>
    `${s} ${OCCURRENCE.filter((o) => o.cls === s).length}`).join(", "));
for (const r of ROLE) console.log(`  ${r}: ${[...scope("separate")[r]].join(", ")}`);
console.log(`  in no one's scope: ` +
  Object.keys(SURFACE).filter((y) => SURFACE[y] === "-").join(", "));

const A = [-33, 9, 9, 11];
console.log("\n1. same 22 occurrences, three observation arrangements");
print(A, "measure", "separate", "selected", "combined");
for (const [ad, k] of [["dev-only scope", "devOnly"],
  ["ops-only scope", "opsOnly"], ["both scopes", "both"],
  ["no scope", "neither"], ["never reaches owner", "unreached"],
  [`owner reach (avg. step)`, "reachStep"], ["noise to dev (occurrences)", "noiseDev"],
  ["noise to ops (occurrences)", "noiseOps"], ["dev load (steps)", "loadDev"],
  ["ops load (steps)", "loadOps"]]) print(A, ad, ...D.map((d) => R[d][k]));

console.log("\n2. four delivery metrics, same window");
print(A, "metric", "separate", "selected", "combined");
for (const [ad, k] of [["deployment frequency (releases)", "releaseCount"],
  ["lead time (avg. step)", "leadTime"], ["change failure rate", "changeFailureRate"],
  ["time to restore (avg. step)", "recovery"]]) print(A, ad, ...D.map((d) => R[d][k]));

const B = [-11, 12, 10, 10, 22];
console.log("\n3. in the separate arrangement, which scope an occurrence class falls into");
print(B, "class", "occurrence", "dev-only", "ops-only", "never reaches owner");
for (const s of ["error", "delay", "drift"]) {
  const g = R.separate.R.G.filter((x) => x.o.cls === s);
  print(B, s, g.length, g.filter((x) => x.g.dev !== null && x.g.ops === null).length,
    g.filter((x) => x.g.ops !== null && x.g.dev === null).length,
    g.filter((x) => x.closeStep === null).length);
}

console.log("\n4. occurrences that never reach their owner in the separate arrangement (ceiling " + CEILING + " steps)");
const C = [-26, -9, 10, 22];
print(C, "occurrence", "owner", "closure", "scope it appears in");
for (const x of R.separate.R.G.filter((x) => x.closeStep === null))
  print(C, x.o.name, x.o.owner, R.separate.R.closure.get(x.o.name) - x.o.born,
    ROLE.filter((r) => x.g[r] !== null).join("+") || "neither");
22 occurrences, 8 observation surfaces; error 10, delay 6, drift 6
  dev: app-record, error-tracking, release-log
  ops: work-schedule, machine-metrics, alert-stream, support-record
  in no one's scope: invoice-statement

1. same 22 occurrences, three observation arrangements
measure                           separate selected   combined
dev-only scope                           1        0          0
ops-only scope                           9        7          0
both scopes                             11       15         22
no scope                                 1        0          0
never reaches owner                      4        1          0
owner reach (avg. step)                7.0      5.0        1.6
noise to dev (occurrences)               1        1          7
noise to ops (occurrences)              10       12         12
dev load (steps)                        95       95        107
ops load (steps)                        54       58         58

2. four delivery metrics, same window
metric                            separate selected   combined
deployment frequency (releases)          5        5          4
lead time (avg. step)                 25.8     27.0       34.8
change failure rate                    40%      20%         0%
time to restore (avg. step)           13.5     11.9       10.2

3. in the separate arrangement, which scope an occurrence class falls into
class        occurrence  dev-only  ops-only   never reaches owner
error                10         0         3                     3
delay                 6         0         4                     0
drift                 6         1         2                     1

4. occurrences that never reach their owner in the separate arrangement (ceiling 30 steps)
occurrence                owner       closure   scope it appears in
duplicate-invoice         dev              42                   ops
negative-consumption      dev              42                   ops
rounding-difference       dev              40               neither
format-break              dev              36                   ops

The numbers are of the measurement kind; their inputs are the assumptions above.

The Same Occurrence, Two Separate Scopes

The first table’s separate column is today’s split. Eleven of the twenty-two occurrences appear in both scopes, nine only in operations’ scope, one only in development’s scope, and one appears in neither scoperounding-difference, which falls on the invoice-statement surface, is on no one’s rotation. The third table breaks this down by class: four of the six delays sit only in operations’ scope, because delay falls on machine metrics and the work schedule; one of the six drifts sits only in development’s scope, because only the development team reads the release log.

The real number is in the bottom row: four occurrences never reach their owner. The fourth table lists them by name, and the owner of all four is the development team. Three of them appear in operations’ scope — they fall on the support record, so the operations team sees them but cannot close them; one appears nowhere. Being surfaced from above takes 30 steps, and their closure falls between 36 and 42 steps. Averaged together with the occurrences that do reach their owner within the same window, the average time for a signal to reach its owner is 7.0 steps.

The ten occurrences in operations’ scope are occurrences operations cannot close; in other words, in the separate arrangement the operations team is already looking at a pile of occurrences it cannot close. A split observation scope does not destroy the signal — it puts it on the wrong side.

The Bill for Combining Scope

The combined column gives both roles all eight surfaces. Visibility becomes complete: all twenty-two occurrences appear in both scopes, zero never reach their owner, and the average reach time drops from 7.0 to 1.6 steps. The cost is in the same column: the noise landing on the development team — occurrences it cannot close — rises from 1 to 7, and its triage load rises from 95 to 107 steps.

Those twelve steps show up in the second table as a delivery metric. Deployment frequency falls from 5 to 4 releases, and lead time rises from 25.8 to 34.8 steps. In exchange, the change failure rate falls from 40% to 0%, and time to restore falls from 13.5 to 10.2 steps. Two of the four metrics improve, and two get worse; a sentence that looks at only one metric cannot carry this decision.

The selected column is a third option: only three of the eight surfaces are shared — the development team sees the support record, and the operations team sees the release log and the invoice statement. Occurrences that never reach their owner fall from 4 to 1, average reach time falls from 7.0 to 5.0 steps, the change failure rate falls from 40% to 20%, and time to restore falls from 13.5 to 11.9 steps. Deployment frequency stays at 5 releases, and lead time rises by only 1.2 steps. The development team’s load does not change at all: 95 steps stays 95 steps. The triage work brought by the three surfaces exactly covers the secondhand context-building cost of the three occurrences that are no longer surfaced from above.

Where the Difference Hides

In this lesson, the difference hides in the split of the observation scope, and its number is this: four of the eight surfaces sit in a single role, one sits in no role; ten of the twenty-two occurrences appear in a single scope, one in neither. The declared part of the difference is the surface split — which team looks at what is known. The undeclared part is which occurrence that split leaves on which side; that is only visible in the run.

How many steps it takes the signal to reach whom also reads from here: in the separate arrangement it reaches its owner in an average of 7.0 steps, in 5.0 steps once three surfaces are shared, and in 1.6 steps once the scopes are combined. This is the technical side of shared responsibility — who the surfaces are opened to; the organizational side is whose flow the triage load of an opened surface is deducted from.

Summary

  • Twenty-two occurrences are split across the separate observation scopes: 11 appear in both scopes, 9 only in operations’ scope, 1 only in development’s scope, 1 in neither; 4 occurrences never reach their owner, and the owner of all four is the development team.
  • The signal reaching its owner takes an average of 7.0 steps in the separate arrangement, 5.0 steps once three surfaces are shared, and 1.6 steps once the scopes are combined.
  • The cost of combining falls on the development team’s flow: noise rises from 1 to 7 occurrences and load rises from 95 to 107 steps; deployment frequency falls from 5 to 4 releases and lead time stretches from 25.8 to 34.8 steps.
  • In exchange, the change failure rate falls from 40% to 0% and time to restore falls from 13.5 to 10.2 steps: two of the four metrics improve, and two get worse.
  • Selectively sharing three surfaces brings most of the improvement (1 unreached, 5.0-step reach, 20% change failure rate) and leaves the development team’s load fixed at 95 steps.

Next Step

When observation scope is split, the signal arrives late or never — that is now measured. But the split is not only in observation: the work itself is split too, and a change gets thrown from team to team. The next lesson runs the same set of changes through two arrangements — separate teams and shared responsibility — and counts the wait per boundary, the changes that bounce back because context could not cross it, and the number of errors that surface in production.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close