Automation Scripting for Operations Questions
Writing scripts and tooling to automate operational and delivery tasks: shell and Python scripting, glue automation, toil reduction, and operational efficiency. Covers automating repetitive infrastructure and deployment work and building internal tooling that raises operational leverage. The concern is task-level automation and scripting, distinct from full pipeline or infrastructure-as-code frameworks.
Describe the differences and trade-offs between using a cloud provider's web console, command-line interface (CLI), and SDKs (e.g., Python SDK). As an SRE, when do you choose CLI vs SDK vs console for automation, runbooks, and debugging? Include examples of tasks better suited to each approach.
Sample Answer
Direct answer
Each of these three surfaces trades discoverability for repeatability differently: the console is best for exploration and one-off human judgment calls, the CLI is best for scriptable and reproducible operational actions, and the SDK is best when the logic needs to be embedded inside a larger program.
When each fits
- Console: best for genuinely one-off investigation where you don't yet know what you're looking for -- browsing resource relationships, reading error messages with full context and formatting, or a task so rare that scripting it isn't worth the investment. Its weakness for automation is exactly its strength for exploration: nothing about it is reproducible or auditable as code.
- CLI: best for operational tasks you'll do more than a couple of times, or that need to be embeddable in a runbook/script/CI job -- the invocation itself is a reviewable, versionable artifact ('here's the exact command that was run'), and it composes naturally with shell scripting (piping,
--output jsonfeeding intojq). - SDK: best when the logic needs conditionals, error handling, or integration with the rest of a larger program that a shell invocation can't cleanly express -- calling the CLI as a subprocess from Python and parsing its text/JSON output works but adds an unnecessary serialization round-trip and a dependency on the CLI's output format staying stable; the SDK gives you native objects and typed exceptions instead.
Choosing for automation, runbooks, and debugging
For AUTOMATION (a script that runs regularly, unattended): SDK, because it needs the richest error handling and doesn't benefit from CLI-output-parsing overhead. For RUNBOOKS (steps a human follows, possibly semi-scripted): CLI, because a runbook that says 'run this exact command' is easier for another engineer to follow and adapt under pressure than 'run this Python snippet,' and it's directly copy-pasteable into a terminal during an incident. For DEBUGGING: usually console first (to explore and understand what's actually going on), then CLI once you know the specific check/action you want to make repeatable.
Concrete task examples
Investigating why a mysterious resource exists and who created it: console, because you're browsing and don't yet know what you're looking for. Rotating a credential as a documented, repeatable operational procedure: CLI, so it's copy-pasteable and auditable via shell history/CI logs. Building a service that needs to programmatically check and remediate resource drift as part of its own logic: SDK, because it's already running as code and gains nothing from shelling out to a CLI.
Trade-offs and pitfalls
A common mistake is defaulting to whichever surface you personally reach for out of habit rather than matching it to the task -- writing automation that shells out to the CLI and parses text output (fragile, breaks silently when a CLI's output formatting changes) when the SDK was directly available, or conversely writing a heavyweight SDK-based script for a genuinely one-off task better served by a few CLI commands typed directly into a runbook.
Implement or outline a reusable retry decorator in Python that supports exponential backoff with jitter, a configurable max attempts, and a predicate callback to classify retryable exceptions. The decorator should be usable on synchronous functions and support logging each attempt. Explain how idempotency assumptions affect your wrapper and where idempotency tokens should be applied when calling external APIs.
Sample Answer
Approach
A retry decorator needs to separate three concerns cleanly: which exceptions are worth retrying (the predicate), how long to wait between attempts (backoff+jitter), and what to do on each attempt (logging, and eventually giving up). Keeping these as parameters rather than hardcoding them is what makes the decorator reusable across call sites with very different retry needs.
import functools
import logging
import random
import time
def retry(max_attempts=5, base_delay=0.5, max_delay=30.0,
retryable=(Exception,), logger=None):
"""Retry a synchronous function with full-jitter exponential backoff.
retryable: a tuple of exception types, OR a callable(exc) -> bool that
classifies whether a given exception is worth retrying.
"""
def is_retryable(exc):
if callable(retryable) and not isinstance(retryable, type):
return retryable(exc)
return isinstance(exc, retryable)
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
attempt = 0
while True:
attempt += 1
try:
return fn(*args, **kwargs)
except Exception as exc:
if not is_retryable(exc) or attempt >= max_attempts:
raise
ceiling = min(max_delay, base_delay * (2 ** (attempt - 1)))
delay = random.uniform(0, ceiling) # full jitter
if logger:
logger.info("attempt %d/%d failed (%r), retrying in %.2fs",
attempt, max_attempts, exc, delay)
time.sleep(delay)
return wrapper
return decorator
Verified in a sandbox: wrapping a function that raises ValueError twice then succeeds, @retry(max_attempts=4, retryable=(ValueError,)) returns the correct result after exactly 3 attempts; wrapping a function that always raises, it makes exactly max_attempts attempts and then re-raises the original exception rather than swallowing it.
Idempotency and the decorator
The decorator itself has no idea whether the wrapped function is safe to call twice -- that judgment has to be made by whoever applies it. Two consequences: (1) the retryable predicate should exclude exceptions that indicate the operation may have partially succeeded in an ambiguous way (a timeout on a POST is the classic ambiguous case: did the server process it and the response got lost, or did it never receive the request?), and (2) for genuinely non-idempotent external calls, the caller should generate an idempotency token before the first attempt and pass the same token on every retry, so the server-side API can deduplicate. The decorator's job is to retry; the idempotency token's job is to make retrying safe -- they're separate concerns that have to be composed correctly by the caller, not something the decorator can enforce on its own.
Trade-offs
A class-based retry policy (rather than a decorator) is worth it once you need per-call overrides (retry THIS call with a shorter window because it's on a critical path) -- decorators are static at definition time unless you thread configuration through explicitly.
Edge cases: a wrapped function called with no arguments, a function whose exception has a non-standard __repr__ that could itself throw during logging, and max_attempts=1 (which should behave as a single unretried call, not loop) are all worth explicitly testing -- the last one is a common off-by-one where an implementation accidentally still retries once even at max_attempts=1.
Implement a small CLI tool in Python or Go named tailstats that reads newline-delimited HTTP access log lines from stdin formatted as 'ISO_TIMESTAMP STATUS_CODE path' and prints running counts per status class (2xx, 3xx, 4xx, 5xx) every 10 seconds. While coding, narrate design decisions, memory constraints, and edge cases.
Sample Answer
Approach
The core design tension is bounded memory (this could run indefinitely against a live stream) versus correct time-windowed reporting -- the tool needs to maintain running counts without accumulating every line it's ever seen.
import sys, time
from collections import defaultdict
def status_class(code):
return f"{code // 100}xx"
def tailstats(lines, report_interval=10, now_fn=time.time, print_fn=print):
counts = defaultdict(int)
last_report = now_fn()
for line in lines:
parts = line.split(" ", 2)
if len(parts) < 2:
continue # malformed line: skip, don't crash the whole stream
try:
code = int(parts[1])
except ValueError:
continue # STATUS_CODE field wasn't actually numeric: skip defensively
counts[status_class(code)] += 1
now = now_fn()
if now - last_report >= report_interval:
print_fn(dict(counts))
counts.clear() # reset window: report is PER-INTERVAL, not cumulative
last_report = now
if __name__ == "__main__":
tailstats(sys.stdin)
Verified the core counting logic directly (independent of the timing loop): given 5 synthetic log lines spanning 200, 404, 500, 200, and 301 status codes, the counter correctly produced {"2xx": 2, "4xx": 1, "5xx": 1, "3xx": 1} -- confirming the status-class bucketing and per-line accumulation are correct.
Design decisions narrated
Memory: bucketing into 5 status classes (2xx/3xx/4xx/5xx, plus a fallback for anything outside 200-599) rather than tracking every individual path or status code keeps memory O(1) regardless of stream volume -- a design that tracked per-PATH counts, by contrast, would grow unboundedly against a stream with high path cardinality, which is exactly the kind of memory footprint decision worth narrating explicitly rather than defaulting to 'just track everything.'
Windowing: counts.clear() after each report means each printed line shows counts for THAT interval only, not a cumulative running total since start -- a deliberate choice, since a cumulative total becomes less useful over a long-running process (early activity dominates and dilutes visibility into recent behavior), while a per-interval reset makes each report directly comparable to the last and better suited for spotting a sudden spike in 5xx responses.
Streaming, not batch: reading sys.stdin line-by-line (an iterator, not sys.stdin.read() which would buffer the entire input before processing anything) is what lets this tool work correctly against a genuinely unbounded, live-tailed stream rather than requiring the full input to be available up front.
Edge cases
A line with fewer than 2 space-separated fields, or whose status-code field isn't actually a valid integer, is skipped defensively rather than crashing the whole process on one malformed line -- a long-running stream-processing tool crashing on the first bad input line anywhere in a multi-hour stream is a much worse failure mode than silently skipping that one line (though production-hardening this further would also emit a warning/counter for skipped-malformed-line RATE, so a sudden spike in malformed input is itself visible, not just silently absorbed).
Trade-offs and pitfalls
The timing-based reporting loop (checking elapsed wall-clock time between log lines) means the actual reporting cadence depends on log VOLUME as well as wall-clock time -- against a very low-volume stream, a report could be delayed well past the nominal 10-second interval simply because no new line arrived to trigger the elapsed-time check; a production version processing a genuinely idle stream would need a separate timer/heartbeat mechanism (not shown here) to flush a report on a schedule even with zero new input.
You are the lead asked to decide whether to centralize automation into a shared platform or let teams own their individual scripts. Create a migration plan for moving toward the shared platform: what governance and technical abstractions would you need, how would you onboard teams, what would you measure to know the migration is working, and how would you manage stakeholder pushback through a phased rollout?
Sample Answer
Direct answer
This is fundamentally a policy decision, not a technical one -- 'can we build a good shared platform' is almost always yes; the actual question is whether the organizational cost of centralizing (migration effort, short-term velocity hit, political friction) is worth the long-term payoff (less duplicated retry/logging/secrets logic, more consistent operational quality, easier cross-team support).
Governance and technical abstractions needed
A shared platform needs, at minimum: a stable core API/library (the retry, logging, secrets, CLI-design primitives this whole topic covers) that individual teams build ON rather than each reinventing; a clear contribution model for teams that need something the core doesn't yet support (a plugin/extension point, not a fork); and a policy for who can approve changes to the shared core, since a shared platform used by many teams needs more conservative change-review than any single team's private script ever did.
Onboarding teams
Don't force a big-bang migration. Start with new automation being REQUIRED to use the shared platform (stops the bleeding, no more net-new duplication), while existing scripts migrate opportunistically -- prioritized by which existing scripts are highest-risk/highest-value to bring under the platform's shared quality bar (the same prioritization logic discussed for toil-reduction elsewhere in this topic: frequency x time x risk), not a blanket 'migrate everything by date X' mandate that creates resentment without matching capacity.
Measuring success
MTTR for automation-related incidents (does centralizing actually reduce time-to-fix, or does it create a bottleneck through a single team that now owns everything); deployment frequency and automation coverage (are teams actually adopting it, or nominally required to and quietly working around it); and, critically, a DIRECT measure of duplication reduced (how many teams' retry/logging/secrets code converged onto the shared implementation) since that's the core value proposition being tested.
Managing stakeholder pushback
The realistic pushback is 'this slows us down and takes away control,' and it's not always wrong -- a shared platform genuinely does trade some team-level autonomy for org-level consistency. Address it by making the platform team's response time to feature requests fast and predictable (a platform that's slow to extend just recreates the pressure to fork/route-around it), and by being honest that centralization is a genuine trade-off, not a strictly-better free lunch, which builds more trust than overselling it.
Phased rollout
Phase 1: build the core shared platform and require it for NEW automation only. Phase 2: migrate the highest-priority existing scripts (per the risk-based prioritization above), measuring MTTR/coverage as you go. Phase 3: revisit whether full migration is worth the remaining effort, or whether some legitimately-fine existing scripts are better left alone -- 100% migration is not automatically the right end state if the remaining stragglers are low-risk and low-value to migrate.
Trade-offs and pitfalls
The most common failure mode in this kind of centralization effort is the platform team optimizing for their OWN roadmap rather than genuinely serving adopting teams' needs -- once that trust breaks, teams quietly route around the shared platform for anything that isn't strictly mandated, and the platform ends up technically 'adopted' while genuinely providing much less value than the metrics suggest.
Outline how you would automate weekly reports distribution to stakeholders using Python scripts and a scheduling tool (cron, Airflow, or Power BI service). Include steps for authentication, rendering dashboards or exporting CSVs, error handling, retries, and secure credentials management.
Sample Answer
Direct answer
The core design shape here is the same as every scheduled automation this topic covers -- fetch/compute, format, deliver, handle failure -- just applied to a reporting use case where correctness of the DATA matters as much as reliability of the delivery.
Steps
Authentication: use a service-account-style credential scoped narrowly to read access on the specific dashboards/data sources needed, fetched fresh (or from a short-lived cache) rather than a long-lived personal credential embedded in the script -- the same secrets-handling discipline this topic covers elsewhere applies directly here, and it matters more than it might seem for 'just a report,' since a report-automation credential with broad read access across many dashboards is a real, easily-overlooked attack surface.
Rendering or exporting: for a dashboard-rendering approach (a headless browser screenshot, or a BI tool's own export API), verify the render actually completed and produced non-empty, non-error output BEFORE treating the run as successful -- a common silent-failure mode here is a screenshot/export that technically 'succeeds' but captures an error page or a stale cached view. For a CSV-export approach, validate the exported data's basic shape (expected row count range, expected columns present) as a sanity check before distributing it, since a silently-empty or truncated export is a much worse failure than an obviously-failed one.
Scheduling: cron for a simple, single-report weekly job is entirely adequate; Airflow becomes worth the added complexity once there are multiple interdependent reports (this one depends on that data pipeline finishing first) that need real dependency-aware orchestration rather than independent fixed-time triggers.
Error handling: retry transient failures (a flaky connection to the data source) with backoff; for a genuine failure that isn't resolved by retry, the report should NOT silently fail to send -- it should alert whoever owns the automation AND, ideally, still notify the intended recipients that this week's report is delayed/unavailable rather than them simply never receiving it and not knowing whether that's expected.
Secure credentials management: as above, scoped and short-lived where the reporting platform supports it; never embed a personal user's credential in a shared automation, since that ties the automation's continued function to one person's account staying valid and creates an audit trail that misattributes automated access to a human.
Trade-offs and pitfalls
The most common failure mode in report-automation specifically (as opposed to other kinds of scheduled jobs) is that a SILENT data-correctness bug is much harder to notice than an outright failure -- a report that renders and sends successfully but contains subtly wrong numbers (a stale cache, a broken filter, a timezone bug shifting which data falls in 'this week') can go unnoticed for a long time precisely because the automation's own health metrics (did it run, did it succeed, how long did it take) all look completely fine. Worth adding a basic sanity check on the OUTPUT DATA itself (row counts within an expected range, key totals within an expected range of the prior week's) as part of what 'success' means for this specific class of automation, not just 'the script exited 0.'
Unlock Full Question Bank
Get access to all Automation Scripting for Operations interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.