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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +98 -0
- data/LICENSE.txt +21 -0
- data/README.md +277 -0
- data/bin/runsheets +12 -0
- data/examples/about-the-examples.md +40 -0
- data/examples/directory_based_runbooks/hello/rollback.md +11 -0
- data/examples/directory_based_runbooks/hello/runbook.md +40 -0
- data/examples/directory_based_runbooks/hello/steps/010-say-hello.md +33 -0
- data/examples/directory_based_runbooks/hello/steps/020-inspect-ruby.md +28 -0
- data/examples/directory_based_runbooks/hello/steps/030-take-a-breath.md +11 -0
- data/examples/directory_based_runbooks/hello/steps/035-keep-a-clock-running.md +21 -0
- data/examples/directory_based_runbooks/hello/steps/040-exercise-failure.md +26 -0
- data/examples/directory_based_runbooks/hello/steps/045-check-the-greeting.md +18 -0
- data/examples/directory_based_runbooks/hello/verify.md +18 -0
- data/examples/directory_based_runbooks/staging-teardown/rollback.md +28 -0
- data/examples/directory_based_runbooks/staging-teardown/runbook.md +57 -0
- data/examples/directory_based_runbooks/staging-teardown/steps/010-confirm-nothing-to-preserve.md +18 -0
- data/examples/directory_based_runbooks/staging-teardown/steps/020-snapshot-the-database.md +47 -0
- data/examples/directory_based_runbooks/staging-teardown/steps/030-stop-the-pipeline.md +32 -0
- data/examples/directory_based_runbooks/staging-teardown/steps/040-drain-the-ecs-services.md +35 -0
- data/examples/directory_based_runbooks/staging-teardown/steps/050-record-the-final-row-counts.md +41 -0
- data/examples/directory_based_runbooks/staging-teardown/steps/060-delete-the-stack.md +56 -0
- data/examples/directory_based_runbooks/staging-teardown/verify.md +40 -0
- data/examples/single_md_file_runbooks/db-maintenance.md +163 -0
- data/examples/single_md_file_runbooks/disk-space-triage.md +90 -0
- data/examples/single_md_file_runbooks/disk-usage-glossary.md +48 -0
- data/examples/single_md_file_runbooks/policies/cleanup-policy.md +33 -0
- data/examples/single_md_file_runbooks/policies/images/triage-flow.svg +44 -0
- data/examples/single_md_file_runbooks/runbooks_with_scripts/.loki +17 -0
- data/examples/single_md_file_runbooks/runbooks_with_scripts/asgard-tasks.md +135 -0
- data/examples/single_md_file_runbooks/runbooks_with_scripts/checks.loki +27 -0
- data/examples/single_md_file_runbooks/runbooks_with_scripts/greet.loki +6 -0
- data/lib/runsheets/assets.rb +780 -0
- data/lib/runsheets/block.rb +118 -0
- data/lib/runsheets/cli.rb +424 -0
- data/lib/runsheets/config/defaults.yml +44 -0
- data/lib/runsheets/config.rb +139 -0
- data/lib/runsheets/diff.rb +67 -0
- data/lib/runsheets/execution.rb +144 -0
- data/lib/runsheets/executor.rb +157 -0
- data/lib/runsheets/expectation.rb +101 -0
- data/lib/runsheets/fences.rb +172 -0
- data/lib/runsheets/front_matter.rb +29 -0
- data/lib/runsheets/library.rb +268 -0
- data/lib/runsheets/pages/chooser.rb +408 -0
- data/lib/runsheets/pages/search.rb +119 -0
- data/lib/runsheets/pages/tree.rb +88 -0
- data/lib/runsheets/pages.rb +947 -0
- data/lib/runsheets/redactor.rb +88 -0
- data/lib/runsheets/renderer.rb +229 -0
- data/lib/runsheets/run.rb +330 -0
- data/lib/runsheets/run_record.rb +371 -0
- data/lib/runsheets/runbook.rb +301 -0
- data/lib/runsheets/search.rb +129 -0
- data/lib/runsheets/session.rb +307 -0
- data/lib/runsheets/session_log.rb +114 -0
- data/lib/runsheets/single_file.rb +93 -0
- data/lib/runsheets/step.rb +91 -0
- data/lib/runsheets/version.rb +5 -0
- data/lib/runsheets/web.rb +537 -0
- data/lib/runsheets.rb +90 -0
- data/sig/runsheets.rbs +4 -0
- 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.
|
data/examples/directory_based_runbooks/staging-teardown/steps/010-confirm-nothing-to-preserve.md
ADDED
|
@@ -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.
|
data/examples/directory_based_runbooks/staging-teardown/steps/050-record-the-final-row-counts.md
ADDED
|
@@ -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.
|