runsheets 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +98 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +277 -0
  5. data/bin/runsheets +12 -0
  6. data/examples/about-the-examples.md +40 -0
  7. data/examples/directory_based_runbooks/hello/rollback.md +11 -0
  8. data/examples/directory_based_runbooks/hello/runbook.md +40 -0
  9. data/examples/directory_based_runbooks/hello/steps/010-say-hello.md +33 -0
  10. data/examples/directory_based_runbooks/hello/steps/020-inspect-ruby.md +28 -0
  11. data/examples/directory_based_runbooks/hello/steps/030-take-a-breath.md +11 -0
  12. data/examples/directory_based_runbooks/hello/steps/035-keep-a-clock-running.md +21 -0
  13. data/examples/directory_based_runbooks/hello/steps/040-exercise-failure.md +26 -0
  14. data/examples/directory_based_runbooks/hello/steps/045-check-the-greeting.md +18 -0
  15. data/examples/directory_based_runbooks/hello/verify.md +18 -0
  16. data/examples/directory_based_runbooks/staging-teardown/rollback.md +28 -0
  17. data/examples/directory_based_runbooks/staging-teardown/runbook.md +57 -0
  18. data/examples/directory_based_runbooks/staging-teardown/steps/010-confirm-nothing-to-preserve.md +18 -0
  19. data/examples/directory_based_runbooks/staging-teardown/steps/020-snapshot-the-database.md +47 -0
  20. data/examples/directory_based_runbooks/staging-teardown/steps/030-stop-the-pipeline.md +32 -0
  21. data/examples/directory_based_runbooks/staging-teardown/steps/040-drain-the-ecs-services.md +35 -0
  22. data/examples/directory_based_runbooks/staging-teardown/steps/050-record-the-final-row-counts.md +41 -0
  23. data/examples/directory_based_runbooks/staging-teardown/steps/060-delete-the-stack.md +56 -0
  24. data/examples/directory_based_runbooks/staging-teardown/verify.md +40 -0
  25. data/examples/single_md_file_runbooks/db-maintenance.md +163 -0
  26. data/examples/single_md_file_runbooks/disk-space-triage.md +90 -0
  27. data/examples/single_md_file_runbooks/disk-usage-glossary.md +48 -0
  28. data/examples/single_md_file_runbooks/policies/cleanup-policy.md +33 -0
  29. data/examples/single_md_file_runbooks/policies/images/triage-flow.svg +44 -0
  30. data/examples/single_md_file_runbooks/runbooks_with_scripts/.loki +17 -0
  31. data/examples/single_md_file_runbooks/runbooks_with_scripts/asgard-tasks.md +135 -0
  32. data/examples/single_md_file_runbooks/runbooks_with_scripts/checks.loki +27 -0
  33. data/examples/single_md_file_runbooks/runbooks_with_scripts/greet.loki +6 -0
  34. data/lib/runsheets/assets.rb +780 -0
  35. data/lib/runsheets/block.rb +118 -0
  36. data/lib/runsheets/cli.rb +424 -0
  37. data/lib/runsheets/config/defaults.yml +44 -0
  38. data/lib/runsheets/config.rb +139 -0
  39. data/lib/runsheets/diff.rb +67 -0
  40. data/lib/runsheets/execution.rb +144 -0
  41. data/lib/runsheets/executor.rb +157 -0
  42. data/lib/runsheets/expectation.rb +101 -0
  43. data/lib/runsheets/fences.rb +172 -0
  44. data/lib/runsheets/front_matter.rb +29 -0
  45. data/lib/runsheets/library.rb +268 -0
  46. data/lib/runsheets/pages/chooser.rb +408 -0
  47. data/lib/runsheets/pages/search.rb +119 -0
  48. data/lib/runsheets/pages/tree.rb +88 -0
  49. data/lib/runsheets/pages.rb +947 -0
  50. data/lib/runsheets/redactor.rb +88 -0
  51. data/lib/runsheets/renderer.rb +229 -0
  52. data/lib/runsheets/run.rb +330 -0
  53. data/lib/runsheets/run_record.rb +371 -0
  54. data/lib/runsheets/runbook.rb +301 -0
  55. data/lib/runsheets/search.rb +129 -0
  56. data/lib/runsheets/session.rb +307 -0
  57. data/lib/runsheets/session_log.rb +114 -0
  58. data/lib/runsheets/single_file.rb +93 -0
  59. data/lib/runsheets/step.rb +91 -0
  60. data/lib/runsheets/version.rb +5 -0
  61. data/lib/runsheets/web.rb +537 -0
  62. data/lib/runsheets.rb +90 -0
  63. data/sig/runsheets.rbs +4 -0
  64. metadata +226 -0
@@ -0,0 +1,28 @@
1
+ ---
2
+ title: Rollback
3
+ ---
4
+
5
+ There is no rolling back a deleted stack; there is rebuilding from the
6
+ snapshot. How far you got decides what to do.
7
+
8
+ **Stopped before step 60 (nothing deleted).** Re-enable the pipeline and
9
+ scale the services back up. The next deploy restores the desired counts;
10
+ this just gets staging answering again sooner:
11
+
12
+ ```bash
13
+ gh workflow enable deploy-staging.yml --repo acme/platform
14
+ for svc in web worker scheduler; do
15
+ aws ecs update-service --cluster "$CLUSTER" --service "$svc" --desired-count 2
16
+ done
17
+ ```
18
+
19
+ **Step 60 failed partway (`DELETE_FAILED`).** The stack is in a state
20
+ CloudFormation will not touch until the failing resource is dealt with.
21
+ Find it with the last block of step 60, remove whatever references it
22
+ outside the stack, and run the delete again. Do not retain resources with
23
+ `--retain-resources` unless you intend to clean them up by hand.
24
+
25
+ **Staging is needed again after step 60.** Restore the snapshot into a
26
+ new instance and point a fresh stack at it. This is the rebuild runbook,
27
+ not this one; the snapshot identifier is `$SNAPSHOT_ID` and the run
28
+ record of the teardown has the row counts to compare against.
@@ -0,0 +1,57 @@
1
+ ---
2
+ title: Staging infrastructure teardown
3
+ when_to_use: >
4
+ The staging environment is being retired or rebuilt from scratch and
5
+ nothing in it needs to survive. Not for routine deploys, not for
6
+ production, never while a release is being verified on staging.
7
+ prerequisites:
8
+ - AWS CLI v2 logged in through SSO to the staging account (`aws sso login`)
9
+ - GitHub CLI (`gh`) authenticated with permission to disable workflows
10
+ - Session Manager plugin for the SSM port forward in step 50
11
+ - jq on the PATH
12
+ blast_radius: >
13
+ Every ECS service, the RDS instance, the load balancer and the DNS record
14
+ for staging. Anything in staging that was not snapshotted in step 20 is
15
+ gone for good. Production is untouched: a different account, a different
16
+ profile.
17
+ escalation: The platform on-call engineer, via the #platform-oncall channel.
18
+ tags: [aws, ecs, rds, staging, destructive]
19
+ inputs:
20
+ - name: AWS_PROFILE
21
+ prompt: AWS profile for the staging account
22
+ default: acme-staging-admin
23
+ - name: AWS_REGION
24
+ prompt: Region the staging stack lives in
25
+ default: us-east-1
26
+ - name: CLUSTER
27
+ prompt: ECS cluster name
28
+ default: acme-staging
29
+ - name: DB_INSTANCE
30
+ prompt: RDS instance identifier
31
+ default: acme-staging-db
32
+ - name: SNAPSHOT_ID
33
+ prompt: Identifier for the final pre-teardown snapshot
34
+ default: acme-staging-final-preteardown
35
+ - name: STACK_NAME
36
+ prompt: CloudFormation stack that owns the environment
37
+ default: acme-staging
38
+ ---
39
+
40
+ # Staging infrastructure teardown
41
+
42
+ This runbook takes the staging environment down in an order that leaves a
43
+ restorable database snapshot and no orphaned resources. It is the shape of
44
+ a real teardown runbook: the commands are the ones you would type, the
45
+ account names are made up.
46
+
47
+ Read the blast radius above before starting a run. The steps are:
48
+
49
+ 1. Confirm with the team that nothing in staging needs to be kept.
50
+ 2. Take and verify a final RDS snapshot.
51
+ 3. Stop the deployment pipeline so nothing redeploys mid-teardown.
52
+ 4. Drain the ECS services to zero.
53
+ 5. Open a tunnel to the database and record the final row counts.
54
+ 6. Delete the CloudFormation stack.
55
+
56
+ Every executed command, its output and your acknowledgements are recorded.
57
+ Attach the run record to the change ticket when you are done.
@@ -0,0 +1,18 @@
1
+ ---
2
+ title: Confirm nothing in staging needs to be preserved
3
+ kind: manual
4
+ ---
5
+
6
+ Post in `#eng-announcements` that staging is being torn down, with the
7
+ change ticket number, and wait for anyone with an in-flight test to
8
+ respond. Give it the time the ticket says, usually an hour.
9
+
10
+ Then check the things people forget:
11
+
12
+ - Feature branches deployed to staging for review. Ask their owners.
13
+ - Data loaded by hand for a demo. If it matters, export it now; the
14
+ snapshot in the next step keeps the database, not S3 uploads.
15
+ - Scheduled jobs that write somewhere outside staging.
16
+
17
+ Mark this step done with a note saying who confirmed and when. That note
18
+ is the audit trail for "we asked".
@@ -0,0 +1,47 @@
1
+ ---
2
+ title: Take a final snapshot of the database
3
+ kind: automated
4
+ timeout: 1800
5
+ ---
6
+
7
+ A manual snapshot survives the deletion of the instance; automated ones do
8
+ not. This is the only copy of staging data that will exist afterwards, so
9
+ wait for it to reach `available` before moving on.
10
+
11
+ ```bash run
12
+ set -euo pipefail
13
+ aws rds create-db-snapshot \
14
+ --db-instance-identifier "$DB_INSTANCE" \
15
+ --db-snapshot-identifier "$SNAPSHOT_ID" \
16
+ --query 'DBSnapshot.{Id:DBSnapshotIdentifier,Status:Status}' --output table
17
+ aws rds wait db-snapshot-available --db-snapshot-identifier "$SNAPSHOT_ID"
18
+ aws rds describe-db-snapshots --db-snapshot-identifier "$SNAPSHOT_ID" \
19
+ --query 'DBSnapshots[0].{Id:DBSnapshotIdentifier,Status:Status,SizeGB:AllocatedStorage,Created:SnapshotCreateTime}' \
20
+ --output table
21
+ ```
22
+
23
+ The first table shows the snapshot `creating`; the second is what you
24
+ want to see, with `Status` at `available`. The borders, the time, the
25
+ size and the snapshot id differ from run to run, so they are `{{any}}`:
26
+
27
+ ```text expect
28
+ {{any}}
29
+ |{{any}}CreateDBSnapshot{{any}}|
30
+ {{any}}
31
+ |{{any}}Id{{any}}Status{{any}}|
32
+ {{any}}
33
+ | {{any}}| creating |
34
+ {{any}}
35
+ {{any}}
36
+ |{{any}}DescribeDBSnapshots{{any}}|
37
+ {{any}}
38
+ |{{any}}Created{{any}}Id{{any}}SizeGB{{any}}Status{{any}}|
39
+ {{any}}
40
+ | {{any}}| available |
41
+ {{any}}
42
+ ```
43
+
44
+ If `create-db-snapshot` fails with `DBSnapshotAlreadyExists`, someone ran
45
+ this step already. Check the existing snapshot's date before reusing it;
46
+ if it is older than today, pick a new `SNAPSHOT_ID` and start the run
47
+ again.
@@ -0,0 +1,32 @@
1
+ ---
2
+ title: Stop the deployment pipeline
3
+ kind: automated
4
+ timeout: 120
5
+ ---
6
+
7
+ Nothing must redeploy to staging while the services are being drained.
8
+ Disable the deploy workflow rather than deleting it, so re-enabling it is
9
+ one command when the environment is rebuilt.
10
+
11
+ ```bash run
12
+ set -euo pipefail
13
+ gh workflow disable deploy-staging.yml --repo acme/platform
14
+ gh workflow list --repo acme/platform --all | grep -i staging
15
+ ```
16
+
17
+ Expect the staging workflow to show `disabled_manually`. The columns are
18
+ separated by tabs or spaces depending on the terminal, and the last one
19
+ is the workflow id:
20
+
21
+ ```text expect
22
+ Deploy to staging{{any}}disabled_manually{{any}}
23
+ ```
24
+
25
+ Some teams also have a bot that re-enables workflows it finds disabled.
26
+ If yours does, pause it in its own UI; that part is interactive, so do it
27
+ in your terminal and confirm here:
28
+
29
+ ```bash terminal
30
+ gh auth status
31
+ open "https://github.com/acme/platform/actions/workflows/deploy-staging.yml"
32
+ ```
@@ -0,0 +1,35 @@
1
+ ---
2
+ title: Drain the ECS services
3
+ kind: automated
4
+ timeout: 900
5
+ ---
6
+
7
+ Scale every service in the cluster to zero and wait for the tasks to stop.
8
+ The load balancer starts returning 503 as soon as the last task exits,
9
+ which is expected; the DNS record goes away with the stack in step 60.
10
+
11
+ ```bash run
12
+ set -euo pipefail
13
+ services=$(aws ecs list-services --cluster "$CLUSTER" --query 'serviceArns[]' --output text)
14
+ test -n "$services" || { echo "no services in $CLUSTER"; exit 0; }
15
+ for svc in $services; do
16
+ name=${svc##*/}
17
+ echo "scaling $name to 0"
18
+ aws ecs update-service --cluster "$CLUSTER" --service "$name" --desired-count 0 --query 'service.desiredCount' --output text
19
+ done
20
+ aws ecs wait services-stable --cluster "$CLUSTER" --services $services
21
+ aws ecs describe-services --cluster "$CLUSTER" --services $services \
22
+ --query 'services[].{Name:serviceName,Desired:desiredCount,Running:runningCount}' --output table
23
+ ```
24
+
25
+ Every row of the final table should read `0` in both `Desired` and
26
+ `Running`. If a service refuses to drain (a task stuck in `DEPROVISIONING`
27
+ for more than ten minutes), stop its tasks directly:
28
+
29
+ ```bash
30
+ aws ecs list-tasks --cluster "$CLUSTER" --service-name web --query 'taskArns[]' --output text \
31
+ | xargs -n1 aws ecs stop-task --cluster "$CLUSTER" --reason "staging teardown" --task
32
+ ```
33
+
34
+ That block is display only on purpose: run it from your terminal against
35
+ the one service that is stuck, not against all of them.
@@ -0,0 +1,41 @@
1
+ ---
2
+ title: Record the final row counts
3
+ kind: verify
4
+ timeout: 300
5
+ ---
6
+
7
+ Before the database goes, write its row counts into the run record. When
8
+ staging is rebuilt from the snapshot, the same query proves the restore
9
+ is complete.
10
+
11
+ The database is only reachable through the bastion. Open a port forward
12
+ and leave it running while the queries run; stop it from the Running
13
+ panel, or let finishing the run stop it:
14
+
15
+ ```bash background
16
+ aws ssm start-session \
17
+ --target "$(aws ec2 describe-instances --filters Name=tag:Role,Values=bastion Name=instance-state-name,Values=running \
18
+ --query 'Reservations[0].Instances[0].InstanceId' --output text)" \
19
+ --document-name AWS-StartPortForwardingSessionToRemoteHost \
20
+ --parameters "{\"host\":[\"$(aws rds describe-db-instances --db-instance-identifier "$DB_INSTANCE" \
21
+ --query 'DBInstances[0].Endpoint.Address' --output text)\"],\"portNumber\":[\"5432\"],\"localPortNumber\":[\"15432\"]}"
22
+ ```
23
+
24
+ With the tunnel up, count the rows in the tables that matter. The password
25
+ comes from the secret in the staging account, never from this file:
26
+
27
+ ```bash run
28
+ set -euo pipefail
29
+ export PGPASSWORD=$(aws secretsmanager get-secret-value --secret-id "staging/db/app" --query SecretString --output text | jq -r .password)
30
+ psql -h localhost -p 15432 -U app -d app -X -At -c "
31
+ select relname, n_live_tup from pg_stat_user_tables order by n_live_tup desc limit 10;"
32
+ ```
33
+
34
+ ```text expect
35
+ events|{{any}}
36
+ orders|{{any}}
37
+ users|{{any}}
38
+ ```
39
+
40
+ The counts are `{{any}}` because they differ every time; the point is
41
+ that the query runs and the record keeps whatever it printed.
@@ -0,0 +1,56 @@
1
+ ---
2
+ title: Delete the CloudFormation stack
3
+ kind: automated
4
+ destructive: true
5
+ timeout: 3600
6
+ ---
7
+
8
+ This is the step that cannot be undone. Everything the stack owns goes:
9
+ the ECS cluster and services, the RDS instance (its final snapshot from
10
+ step 20 stays), the load balancer, the DNS record, the security groups.
11
+
12
+ Check once more that the snapshot exists and is `available`, in the same
13
+ process that will delete the stack, so a wrong `AWS_PROFILE` fails here
14
+ rather than on the delete:
15
+
16
+ ```bash run
17
+ set -euo pipefail
18
+ aws sts get-caller-identity --query '{Account:Account,Arn:Arn}' --output table
19
+ aws rds describe-db-snapshots --db-snapshot-identifier "$SNAPSHOT_ID" --query 'DBSnapshots[0].Status' --output text
20
+ ```
21
+
22
+ The caller identity table comes first, then the snapshot status. The
23
+ account and ARN are yours to check by eye, so they are `{{any}}` here:
24
+
25
+ ```text expect
26
+ {{any}}
27
+ |{{any}}GetCallerIdentity{{any}}|
28
+ {{any}}
29
+ |{{any}}Account{{any}}Arn{{any}}|
30
+ {{any}}
31
+ | {{any}}| arn:aws:{{any}}|
32
+ {{any}}
33
+ available
34
+ ```
35
+
36
+ Then delete. The confirmation code the page asks for is a speed bump; the
37
+ real check is the table above showing the staging account, not
38
+ production.
39
+
40
+ ```bash destructive
41
+ set -euo pipefail
42
+ aws cloudformation delete-stack --stack-name "$STACK_NAME"
43
+ aws cloudformation wait stack-delete-complete --stack-name "$STACK_NAME"
44
+ echo "stack $STACK_NAME deleted"
45
+ ```
46
+
47
+ `wait stack-delete-complete` can take forty minutes for a stack with an
48
+ RDS instance in it; the step timeout allows an hour. If it fails with
49
+ `DELETE_FAILED`, the events tell you which resource refused, usually a
50
+ security group something outside the stack still references:
51
+
52
+ ```bash run
53
+ aws cloudformation describe-stack-events --stack-name "$STACK_NAME" \
54
+ --query 'StackEvents[?ResourceStatus==`DELETE_FAILED`].{Resource:LogicalResourceId,Reason:ResourceStatusReason}' \
55
+ --output table
56
+ ```
@@ -0,0 +1,40 @@
1
+ ---
2
+ title: Verify
3
+ kind: verify
4
+ timeout: 300
5
+ ---
6
+
7
+ Run after the last step, or on their own from the **Checks** page at any
8
+ time afterwards: these prove staging is really gone and the snapshot is
9
+ really there.
10
+
11
+ ```bash run
12
+ set -euo pipefail
13
+ status=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" --query 'Stacks[0].StackStatus' --output text 2>&1 || true)
14
+ case "$status" in
15
+ *"does not exist"*) echo "stack $STACK_NAME: gone" ;;
16
+ *) echo "stack $STACK_NAME still reports: $status"; exit 1 ;;
17
+ esac
18
+ ```
19
+
20
+ ```bash run
21
+ set -euo pipefail
22
+ aws ecs describe-clusters --clusters "$CLUSTER" --query 'clusters[0].status' --output text | grep -qx INACTIVE \
23
+ && echo "cluster $CLUSTER: inactive" || { echo "cluster $CLUSTER is still active"; exit 1; }
24
+ ```
25
+
26
+ ```bash run
27
+ set -euo pipefail
28
+ aws rds describe-db-snapshots --db-snapshot-identifier "$SNAPSHOT_ID" \
29
+ --query 'DBSnapshots[0].{Status:Status,SizeGB:AllocatedStorage,Encrypted:Encrypted}' --output table
30
+ ```
31
+
32
+ ```text expect
33
+ --------------------------------------
34
+ | DescribeDBSnapshots |
35
+ +-----------+----------+-------------+
36
+ | Encrypted | SizeGB | Status |
37
+ +-----------+----------+-------------+
38
+ | True | {{any}} | available |
39
+ +-----------+----------+-------------+
40
+ ```
@@ -0,0 +1,163 @@
1
+ ---
2
+ title: Monthly PostgreSQL maintenance
3
+ when_to_use: >
4
+ The first Monday of the month, or when the bloat report in the monitoring
5
+ dashboard crosses 30% on any table. Run against one database at a time.
6
+ prerequisites:
7
+ - psql 14 or newer on the PATH
8
+ - A role with ownership of the application tables (VACUUM and REINDEX need it)
9
+ - Nobody running a long report; REINDEX takes a lock
10
+ escalation: The data platform channel; after hours, the database on-call.
11
+ tags: [postgres, maintenance]
12
+ inputs:
13
+ - name: PGHOST
14
+ prompt: Database host
15
+ default: localhost
16
+ - name: PGPORT
17
+ prompt: Port
18
+ default: "5432"
19
+ - name: PGDATABASE
20
+ prompt: Database name
21
+ default: app
22
+ - name: PGUSER
23
+ prompt: Role to connect as
24
+ default: app_owner
25
+ - name: PGPASSWORD
26
+ prompt: Password for that role
27
+ secret: true
28
+ interpreters:
29
+ sql: psql -X -v ON_ERROR_STOP=1 --pset footer=off -f
30
+ ---
31
+
32
+ # Monthly PostgreSQL maintenance
33
+
34
+ A single-file runbook: this one markdown file holds the front matter, this
35
+ preamble, and one `##` heading per step. It also shows `sql run` blocks.
36
+ They execute because the front matter maps `sql` to a `psql` command; the
37
+ file runsheets writes each block to (`<block-id>.<n>.cmd` in the run
38
+ directory) is appended to that command, and `psql` picks the connection
39
+ up from the `PG*` inputs in the environment. The password is a secret input: it reaches `psql`, never the
40
+ run record, and is redacted if anything prints it.
41
+
42
+ Step attributes live in an HTML comment right after each heading. GitHub
43
+ and every other renderer hide the comment, so the file reads as ordinary
44
+ markdown anywhere else.
45
+
46
+ ## Check the connection
47
+ <!-- kind: verify, timeout: 30 -->
48
+
49
+ Prove the inputs are right before anything else. This also runs on its
50
+ own from the Checks page.
51
+
52
+ ```sql run
53
+ select current_database() as database, current_user as role, version();
54
+ ```
55
+
56
+ The version string, and so the width of its column, depends on the
57
+ server; `{{any}}` stands in for both:
58
+
59
+ ```text expect
60
+ database | role |{{any}}version
61
+ ----------+-----------+{{any}}
62
+ app | app_owner | PostgreSQL {{any}}
63
+ ```
64
+
65
+ ## Find the bloated tables
66
+ <!-- kind: automated, timeout: 120 -->
67
+
68
+ Dead tuples that autovacuum has not caught up with. Anything with more
69
+ than a fifth of its rows dead is a candidate for the next step.
70
+
71
+ ```sql run
72
+ select relname,
73
+ n_live_tup as live,
74
+ n_dead_tup as dead,
75
+ round(100.0 * n_dead_tup / greatest(n_live_tup + n_dead_tup, 1), 1) as dead_pct,
76
+ last_autovacuum
77
+ from pg_stat_user_tables
78
+ where n_dead_tup > 10000
79
+ order by dead_pct desc
80
+ limit 15;
81
+ ```
82
+
83
+ Note the worst table in the step note; the next step takes its name as a
84
+ literal because `VACUUM` cannot be parameterised.
85
+
86
+ ## Vacuum the worst table
87
+ <!-- kind: automated, timeout: 1800 -->
88
+
89
+ `VACUUM (VERBOSE, ANALYZE)` does not take an exclusive lock and can run
90
+ while the application is up. Edit the table name before clicking Run;
91
+ the exact text that ran is what the record keeps.
92
+
93
+ ```sql run
94
+ vacuum (verbose, analyze) events;
95
+ ```
96
+
97
+ Then confirm the dead count dropped:
98
+
99
+ ```sql run
100
+ select relname, n_live_tup, n_dead_tup, last_vacuum
101
+ from pg_stat_user_tables
102
+ where relname = 'events';
103
+ ```
104
+
105
+ ## Reindex
106
+ <!-- kind: automated, destructive: true, timeout: 3600 -->
107
+
108
+ `REINDEX` takes a lock that blocks writes to the table for its duration.
109
+ It is marked destructive so the page asks for a confirmation code and
110
+ shows the escalation contact: the harm is downtime, not data loss.
111
+
112
+ ```sql destructive
113
+ reindex (verbose) table concurrently events;
114
+ ```
115
+
116
+ If `CONCURRENTLY` fails partway it leaves an invalid index behind. Find
117
+ and drop it:
118
+
119
+ ```sql run
120
+ select indexrelid::regclass as index, indisvalid
121
+ from pg_index
122
+ where indrelid = 'events'::regclass and not indisvalid;
123
+ ```
124
+
125
+ ## Tell the team
126
+ <!-- kind: manual -->
127
+
128
+ Post the before and after dead-tuple counts from steps 2 and 3 in the
129
+ data platform channel. Mark this step done with a link to the post.
130
+
131
+ ## Verify
132
+
133
+ Whole-procedure checks. Everything here is read-only and safe to run at
134
+ any time from the Checks page.
135
+
136
+ ```sql run
137
+ select count(*) filter (where not indisvalid) as invalid_indexes
138
+ from pg_index;
139
+ ```
140
+
141
+ ```text expect
142
+ invalid_indexes
143
+ -----------------
144
+ 0
145
+ ```
146
+
147
+ ```sql run
148
+ select relname, n_dead_tup
149
+ from pg_stat_user_tables
150
+ where n_dead_tup > 100000
151
+ order by n_dead_tup desc;
152
+ ```
153
+
154
+ ## Rollback
155
+
156
+ Nothing here destroys data. A `VACUUM` interrupted partway has done part
157
+ of its work and left the rest for next time. A `REINDEX CONCURRENTLY`
158
+ interrupted partway leaves an invalid index; drop it with the query in
159
+ the Reindex step and run the reindex again when there is a quiet window:
160
+
161
+ ```sql
162
+ drop index concurrently events_created_at_idx_ccnew;
163
+ ```
@@ -0,0 +1,90 @@
1
+ ---
2
+ title: Disk space triage
3
+ when_to_use: >
4
+ A host is close to full: an alert on free space, a build failing with
5
+ "No space left on device", or a log directory that keeps growing.
6
+ prerequisites:
7
+ - A shell on the host, as a user that can read the directory to check
8
+ - The cleanup policy (linked below) open in another tab
9
+ escalation: The owner of the host; for shared build machines, the platform channel.
10
+ tags: [disk, safe, example]
11
+ inputs:
12
+ - name: TARGET_DIR
13
+ prompt: Directory to check
14
+ default: /tmp
15
+ - name: THRESHOLD
16
+ prompt: Highest acceptable use, in percent
17
+ default: "90"
18
+ ---
19
+
20
+ # Disk space triage
21
+
22
+ A single-file runbook that leans on plain documents kept elsewhere in the
23
+ examples directory. The background lives there, not in the steps:
24
+
25
+ - [Disk usage glossary](disk-usage-glossary.md): what `df` and `du`
26
+ report and why they can disagree.
27
+ - [Cleanup policy](policies/cleanup-policy.md): what may be deleted, and what
28
+ must never be.
29
+
30
+ Those files have no front matter, so runsheets treats them as documents:
31
+ they stay out of the library tree, and the links above open them as pages.
32
+ A document can sit anywhere under the directory `runsheets` was started on;
33
+ one is beside this file, the other in `policies/`. See also
34
+ [about these examples](../about-the-examples.md).
35
+
36
+ Every block in this runbook only reads; nothing is deleted.
37
+
38
+ ## Check free space
39
+ <!-- kind: verify, timeout: 30 -->
40
+
41
+ How full is the file system that holds `TARGET_DIR`? `df` answers for the
42
+ whole file system, not the directory; see
43
+ [df and du](disk-usage-glossary.md#df-and-du) if that is surprising.
44
+
45
+ ```bash run
46
+ df -h "$TARGET_DIR"
47
+ ```
48
+
49
+ ## Find what is using it
50
+ <!-- timeout: 120 -->
51
+
52
+ The ten largest entries directly under `TARGET_DIR`. Entries you cannot
53
+ read are skipped.
54
+
55
+ ```bash run
56
+ du -sh "$TARGET_DIR"/* 2>/dev/null | sort -rh | head -10
57
+ ```
58
+
59
+ If the total here is far below what `df` reported, the space is held
60
+ somewhere else: another directory on the same file system, or deleted files
61
+ a process still holds open. The glossary explains
62
+ [deleted-but-open files](disk-usage-glossary.md#deleted-but-open-files).
63
+
64
+ ## Decide what to clean
65
+ <!-- kind: manual -->
66
+
67
+ Compare what you found with the [cleanup policy](policies/cleanup-policy.md).
68
+ Delete only what it lists as safe, in your own terminal, and note here what
69
+ you removed and how much it freed.
70
+
71
+ ```bash
72
+ rm -rf "$TARGET_DIR"/some-build-cache
73
+ ```
74
+
75
+ ## Verify
76
+
77
+ Use is at or below the threshold.
78
+
79
+ ```bash run
80
+ used=$(df -P "$TARGET_DIR" | awk 'NR == 2 { sub("%", "", $5); print $5 }')
81
+ echo "use ${used}%, threshold ${THRESHOLD}%"
82
+ test "$used" -le "$THRESHOLD"
83
+ ```
84
+
85
+ ## Rollback
86
+
87
+ Nothing in this runbook deletes anything. If a manual cleanup removed
88
+ something it should not have, restore it from backup and tell the owner of
89
+ the host; the [cleanup policy](policies/cleanup-policy.md#when-something-was-deleted-by-mistake)
90
+ says who to call.
@@ -0,0 +1,48 @@
1
+ # Disk usage glossary
2
+
3
+ Background for the [disk space triage](disk-space-triage.md) runbook.
4
+ This file has no front matter, so runsheets treats it as a plain document:
5
+ it is not in the library tree, and nothing on this page executes.
6
+
7
+ ## File system and mount point
8
+
9
+ A disk, or a slice of one, is formatted as a **file system** and attached
10
+ to the directory tree at a **mount point**. Everything below a mount point
11
+ lives on that file system until another mount point takes over.
12
+
13
+ ```bash
14
+ df -h / # the file system mounted at /
15
+ mount | head # what is mounted where
16
+ ```
17
+
18
+ ## df and du
19
+
20
+ `df` asks the file system how many blocks are in use. `du` walks a directory
21
+ and adds up the sizes of the files it can see. They answer different
22
+ questions:
23
+
24
+ - `df -h /tmp` reports the whole file system that holds `/tmp`, including
25
+ everything outside `/tmp`.
26
+ - `du -sh /tmp` reports only what is under `/tmp`, and only what you can
27
+ read.
28
+
29
+ So `du` coming in well below `df` is normal. A large gap is worth a look.
30
+
31
+ ## Deleted-but-open files
32
+
33
+ Deleting a file removes its name. The space is freed only when no process
34
+ still has the file open. A log file deleted while a server is writing to it
35
+ keeps growing, invisible to `du`, until the server is restarted or reopens
36
+ its logs.
37
+
38
+ ```bash
39
+ lsof +L1 # open files with no remaining name
40
+ ```
41
+
42
+ ## Inodes
43
+
44
+ Every file uses one inode. A file system can run out of inodes with plenty
45
+ of free blocks, usually from millions of tiny files. `df -i` shows inode
46
+ use.
47
+
48
+ See the [cleanup policy](policies/cleanup-policy.md) for what to do about any of it.