The opinion
Every team collects a folder of scripts. They run once, and eighteen months later someone runs one against the wrong account because nothing in it said not to.
An ops script is a small production system with no monitoring, run by a tired person under pressure.
So idempotency, dry runs, and clear output are the minimum.
Python is my default because the cloud SDKs are good and the code is readable by whoever inherits it.
Safe to run twice
Scripts get interrupted by timeouts, dropped sessions, and sleeping laptops. If a second run is dangerous, every interruption becomes a manual cleanup. Describe the end state and converge on it.
def add_tag(client, resource_ids, tag):
for rid in resource_ids:
client.create_tags(Resources=[rid], Tags=[tag])
counter.increment() # double-counts on re-run
def ensure_tag(client, resource_ids, tag) -> list[str]:
"""Ensure `tag` is present. Returns only the resources changed."""
changed = []
for rid in resource_ids:
current = get_tags(client, rid)
if current.get(tag["Key"]) == tag["Value"]:
continue # already correct
client.create_tags(Resources=[rid], Tags=[tag])
changed.append(rid)
return changed
The second version can run in a loop without damage. It reports what it changed, not what it tried.
Dry run by default, apply on purpose
Anything that changes infrastructure should show what it would do unless told otherwise. A mistyped command then does no harm.
parser.add_argument("--apply", action="store_true",
help="actually make changes (default: dry run)")
parser.add_argument("--profile", required=True,
help="AWS profile (no default, on purpose)")
args = parser.parse_args()
# Say out loud which account you're about to touch.
identity = session.client("sts").get_caller_identity()
log.info("account=%s mode=%s", identity["Account"],
"APPLY" if args.apply else "dry-run")
if not args.apply:
log.info("dry run: no changes will be made")
--profile has no default. A default lets whatever credentials are in the environment run against production by accident. Printing the account first is a two-line habit that has saved me more than once.
Write output for the person reading it at 2am
- Log what changed, not what was attempted. "Tagged 3 of 47, 44 already correct" beats "Processing…"
- Print a summary at the end. Nobody scrolls back through 400 lines.
- Exit non-zero on failure, and say what to do next.
- Use
logging, notprint.
- Do not swallow exceptions to keep a loop going. A script that reports success after skipping ten resources is worse than one that crashes.
When a script graduates
- Ad hoc: solve it interactively.
- Script: it happened twice. Make it idempotent, dry-run, and logged.
- Scheduled: it recurs. Run it on a schedule and report.
- Eliminated: ask why it happens at all. The best outcome is fixing the cause and deleting the script.
Step four is the one people skip. Automation can quietly remove the pressure that would have led to the real fix.
Anti-patterns
- Credentials in the script. Use profiles, roles, or the environment.
- No
--dry-runon a destructive script. Adding one takes minutes. - Hardcoded account IDs or regions. The script gets copied and edited, and now there are two.
except: pass. A loud failure becomes a silent wrong answer.- No header comment. One sentence: what it does, what it needs, what it changes.
With Claude
Writing scripts is one of the clearest wins, if you specify the safety properties up front.
Ask for the properties, not just the function
Write a Python script that finds unattached EBS volumes across a set
of AWS accounts and reports their monthly cost.
Requirements:
- read-only; no delete path in this script at all
- --profile is required, no default
- print the resolved account ID before doing any work
- paginate properly; assume thousands of volumes
- summary table at the end: account, count, total monthly cost
- use logging, not print
Handle the case where a profile's credentials have expired by naming
which profile failed and continuing with the rest.
The last paragraph is the part people leave out. It bites on run one, when one session has quietly expired.
Hardening an existing script
Review this script as an operational safety audit. Assume it will be
run at 2am by someone who did not write it.
Check:
- is it idempotent? if not, what breaks on a second run?
- is there a dry-run path, and is it the default?
- can it target the wrong account? how would the operator know?
- are failures visible, or swallowed?
- does the output say what CHANGED, or only what it attempted?
Give me a table of findings, then the patched version of the riskiest
one only. Don't rewrite the whole thing.
Generating the runbook with the script
The session already has what you need to document the script. Ask for the runbook before you close it.
Read-only scripts from a model can run freely. Read every line of anything that changes state.
A script is the artifact that will run later, by someone without the context that produced it.