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.
Explain what idempotency means in the context of infrastructure automation. You are writing a Python script that must ensure the directory '/etc/myapp' and a configuration file '/etc/myapp/config.yaml' with exact contents exist on many remote hosts. Describe design choices that make the script idempotent, how to detect divergence, how to perform atomic updates to avoid partial writes, how to avoid race conditions when multiple agents run concurrently, and sketch concise pseudocode or Python usage showing checks and atomic file writes.
Sample Answer
Direct answer
In infrastructure automation, idempotency means running the script against a host that's already in the desired state produces no changes and no errors -- the script converges toward a target state rather than blindly re-applying a sequence of actions. For ensuring a directory and a config file exist with exact contents on many hosts, that means the script has to check current state, act only on the delta, and never assume it's the first time it's ever run there.
Design choices for idempotency
Check before creating the directory (os.makedirs(path, exist_ok=True) handles the 'already exists' case cleanly rather than erroring); for the config file, don't just check EXISTENCE, compare CONTENT -- a file that exists but has stale content still needs to be updated, so the idempotency check has to be 'does the current content match the desired content,' not just 'does a file exist at this path.'
Detecting divergence and atomic updates
import hashlib, os, tempfile
def ensure_config(path: str, desired_bytes: bytes) -> bool:
"""Returns True if a change was made, False if already correct (no-op)."""
if os.path.exists(path):
with open(path, 'rb') as f:
if f.read() == desired_bytes:
return False # already correct, nothing to do
d = os.path.dirname(path)
fd, tmp_path = tempfile.mkstemp(dir=d, prefix='.tmp-')
try:
with os.fdopen(fd, 'wb') as f:
f.write(desired_bytes)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, path) # atomic on POSIX: no reader ever sees a partial file
except Exception:
os.unlink(tmp_path)
raise
return True
Verified in a sandbox: calling ensure_config twice in a row with identical desired content reports changed=True on the first call and changed=False on the second, and the file's final bytes match the desired content exactly. A second test simulated a crash mid-write by leaving an orphaned .tmp-* file in the directory and confirmed the real config file was completely untouched by it -- the atomic-rename pattern means a crash during writing never corrupts or partially-overwrites the file a reader (or the next run) sees.
Avoiding races when multiple agents run concurrently
Write-then-os.replace() is atomic at the OS level for a SINGLE writer, but if two agents on the same host race to update the same file simultaneously, the LAST rename wins and the other agent's write is silently discarded (not corrupted, just lost) -- usually acceptable if both are converging toward the same desired state and would compute identical desired_bytes anyway, but worth naming explicitly since 'atomic' doesn't mean 'coordinated.' If genuinely concurrent, conflicting writers are possible, add a file lock (flock) around the read-compare-write sequence, or route all writes for a given path through a single process/queue rather than letting arbitrary agents write directly.
Pseudocode summary
ensure_directory(path):
makedirs(path, exist_ok=True)
ensure_config_file(path, desired_content):
if read(path) == desired_content: return NO_CHANGE
atomic_write(path, desired_content)
return CHANGED
Edge cases: a directory that exists but as the WRONG type (a file sitting at the path where a directory is expected) will make os.makedirs(path, exist_ok=True) raise rather than silently succeed -- worth an explicit, clearer error for this case rather than letting a confusing FileExistsError/NotADirectoryError surface unexplained. A desired-content byte string containing content that looks identical after a lossy encoding round-trip (rare, but possible with certain file encodings) can make the content-comparison check pass when the actual desired semantic content differs.
Trade-offs and pitfalls
The atomic-write-plus-content-compare pattern shown here trades a small amount of extra I/O (a read-and-compare before every write) for a strong safety guarantee; for a VERY high-frequency check (called thousands of times a second) that overhead would matter and a cheaper pre-check (a stored hash rather than a full read) would be worth the added bookkeeping complexity.
Describe recommended approaches to package and distribute automation tooling so other teams can consume it safely: compare publishing a pip package (wheel), shipping a static Go binary, or distributing a Docker container. Discuss artifact repositories, semantic versioning, documentation, and installability on minimal OS images.
Sample Answer
Direct answer
The three options trade artifact simplicity for runtime dependency management, and the right choice usually depends on who's consuming the tool and what environments they run in.
Comparison
- pip package (wheel): Best when consumers are already Python environments (other services, CI jobs, developer machines with a Python toolchain). Lowest friction for Python-native consumers, but requires them to manage a compatible Python version and a virtual environment -- friction for anyone NOT already in a Python-heavy workflow.
- Static Go binary: Best for cross-team distribution where you can't assume a specific runtime is installed -- a single binary with no dependencies just works, which matters enormously for a tool that needs to run on minimal container images or across heterogeneous developer machines.
- Docker container: Best when the tool has non-trivial dependencies beyond its own code (system libraries, specific tool versions) that would otherwise need documenting and manually installing -- the container bundles the whole environment, at the cost of requiring Docker itself to be available and usually being the heaviest option to distribute and run for a simple CLI invocation.
Artifact repositories, versioning, documentation
Whichever format, publish to a proper artifact repository (a private PyPI index, an internal container registry, a binary artifact store) rather than ad-hoc file shares -- this is what gives you a queryable history of every version ever shipped and lets consumers pin exact versions. Use semantic versioning (MAJOR.MINOR.PATCH) so consumers can reason about upgrade risk from the version number alone: a MINOR bump should never break their existing usage, a MAJOR bump signals 'read the changelog before upgrading.' Documentation should live alongside the artifact (a README in the package, not a separate wiki page that drifts out of sync) and always include a minimal working example, not just a flag reference.
Installability on minimal OS images
A wheel needs a matching Python version and its dependencies resolved, which can fail silently-ish on a minimal image missing system libraries a dependency needs (common with anything using C extensions). A static Go binary sidesteps this entirely -- copy it in, run it, no runtime to match. A container sidesteps it differently -- the image carries its own complete environment -- at the cost of needing a container runtime present and typically being much larger to pull.
Combined pip + Docker distribution
Worth naming the two aren't mutually exclusive: publishing BOTH a pip package (for Python-native consumers who want to import it as a library, not just run it as a CLI) and a Docker image (for consumers who just want to run the tool without any Python setup at all) is common and reasonable for tools that serve both audiences. Making that work well requires the same version number to map cleanly across both artifact types, pinned dependencies baked into the image at build time (not resolved fresh on every container start, which breaks reproducibility), and a CI build step that produces both artifacts from the same tagged commit so they can never drift apart into 'the wheel and the image are technically different versions.'
Trade-offs and pitfalls
The most common mistake is picking based on what's easiest to build rather than what's easiest for CONSUMERS to install -- a wheel is trivial to build but pushes real dependency-resolution risk onto every consumer's environment; a container is heavier to build and distribute but removes almost all of that risk from the consumer's side.
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.
During a release, rollbacks failed because automation couldn't fetch required secrets (secrets had been rotated or were missing). Describe immediate mitigation steps to restore rollbacks safely, and propose design changes to make rollback automation resilient to secret failures (fallback credentials, local cached encrypted secrets, staged rotation). Also propose CI/policy changes to prevent future secret-related rollback failures.
Sample Answer
Direct answer
The immediate priority is restoring the ability to roll back safely -- even if that means a manual, out-of-band credential retrieval -- because a release stuck mid-rollout with no working rollback path is a worse incident than the original release problem.
Immediate mitigation
First, determine whether the secret truly can't be fetched (Vault/secrets-manager outage, network partition) or was actually rotated out from under the rollback automation (a process/timing bug, not an outage) -- these need different responses. If it's an outage, escalate to whoever owns the secrets infrastructure while simultaneously checking for a legitimate emergency-access path (a break-glass credential, held under strict audit controls, specifically for exactly this situation) rather than waiting indefinitely. If it's a rotation-timing bug, the fastest safe fix is often to manually fetch the currently-valid credential and inject it for this one rollback, while treating the underlying timing bug as the real root cause to fix afterward, not something to patch over silently.
Design changes for resilience
Fallback credentials: for the SPECIFIC case of rollback (a safety-critical, time-sensitive operation), consider maintaining a separate, more conservatively-rotated credential path used only for rollback, so rollback's credential lifecycle isn't coupled to the same rotation cadence/timing as normal deploy-time credentials. Locally cached encrypted secrets: cache the credential rollback needs, encrypted at rest, refreshed on a schedule, so a live secrets-manager outage at the exact moment of a rollback doesn't block the rollback entirely -- the cache trades a small staleness window for availability specifically in the failure mode that matters most (needing to roll back FAST, precisely when other things are already going wrong). Staged rotation: rotate credentials with an overlap window where both the old and new credential remain valid for some period, rather than a hard cutover -- this closes the exact race condition (rotation happens mid-rollback-attempt) that likely caused this incident in the first place.
CI/policy changes
Add an explicit pre-flight check to the rollback path itself: before beginning a rollback, verify the credential it will need is actually fetchable, and fail fast with a clear error if not, rather than discovering the gap partway through an already-in-progress rollback. Add a policy that any credential rotation affecting a system used by rollback automation must go through a change window that doesn't overlap active deploys, and add rollback-path secret-fetching to the automation's own regular testing/game-day exercises -- if rollback is only ever tested during the actual moment it's needed, its own dependencies (like this one) won't get caught until they cause a real incident.
Trade-offs and pitfalls
The locally-cached-encrypted-secret fallback is a genuine security trade-off, not a free win -- caching credentials anywhere, even encrypted, widens the attack surface compared to always fetching fresh, so it should be scoped narrowly (rollback-path-only, short cache lifetime, and itself subject to the same audit/rotation discipline as any other credential store) rather than becoming a general-purpose 'cache everything to avoid outages' pattern.
You have a pipeline of automation steps: provision VMs, deploy service, migrate DB, update DNS. Design a script-based orchestrator (not a full workflow engine) that runs these steps in order, records state so it can resume after failures, supports compensating rollback for each step, and exposes run status for operators. Describe data structures, state persistence, idempotency requirements, and how to implement resume and manual intervention.
Sample Answer
Direct answer
The defining constraint is explicitly 'not a full workflow engine' -- this needs enough structure to be safe (resumable, rollback-capable, observable) without the complexity of a general-purpose orchestration platform, which argues for a small, purpose-built state machine over adopting or building something heavier.
Data structures
from dataclasses import dataclass, field
from enum import Enum
class StepStatus(Enum):
PENDING = "pending"
RUNNING = "running"
DONE = "done"
FAILED = "failed"
ROLLED_BACK = "rolled_back"
@dataclass
class Step:
name: str
action: callable
compensate: callable
status: StepStatus = StepStatus.PENDING
@dataclass
class RunState:
run_id: str
steps: list # ordered list of Step, execution order == list order
current_index: int = 0
State persistence and resume
Persist RunState (as JSON, keyed by run_id) after EVERY step transition, not just at the end -- this is what makes resume-after-failure possible: on restart, load the persisted state, find the first step not yet DONE, and continue from exactly there rather than from the beginning. This is the same durable-checkpoint pattern demonstrated and verified elsewhere in this topic for resumable long-running automation, applied here at the step-sequence level rather than the per-item level.
Idempotency requirements
Each step's action MUST be idempotent (or resume could re-execute a step that actually completed but crashed before its status was persisted as DONE) -- 'provision VMs' needs to check-then-create rather than blindly create, 'update DNS' needs to set the record to the desired value rather than blindly append, following the same idempotency discipline covered throughout this topic. This requirement is non-negotiable for a resumable orchestrator: without it, a crash-and-resume can silently double-apply a step that only appeared to fail.
Resume and compensating rollback
def run(state: RunState):
for i in range(state.current_index, len(state.steps)):
step = state.steps[i]
step.status = StepStatus.RUNNING
persist(state)
try:
step.action()
step.status = StepStatus.DONE
state.current_index = i + 1
persist(state)
except Exception as e:
step.status = StepStatus.FAILED
persist(state)
_rollback(state, up_to_index=i)
raise RuntimeError(f"orchestration failed at step '{step.name}'") from e
def _rollback(state, up_to_index):
for i in reversed(range(up_to_index)):
step = state.steps[i]
if step.status == StepStatus.DONE:
step.compensate()
step.status = StepStatus.ROLLED_BACK
persist(state)
This mirrors the reverse-order compensation logic verified elsewhere in this topic for saga-style step coordination: only steps that actually completed (DONE) get compensated, in the reverse of their completion order, and each compensation is itself persisted so a crash MID-rollback can also resume correctly rather than needing to restart the whole rollback from scratch.
Manual intervention and run status
Expose RunState via a simple status query (which step is the run currently on, what's its history) so an operator can see exactly where a stuck or failed run is without reading logs, and support an explicit 'mark this step done manually' override for the case where a step's real-world effect actually succeeded but the automation's own tracking got out of sync (a manual DNS change made out-of-band during an incident, say) -- without this escape hatch, a genuinely-fine-in-reality but confused-in-state run has no path forward except editing the persisted state file by hand.
Trade-offs and pitfalls
The most common design mistake is persisting state only at run COMPLETION rather than after every individual step transition -- that shortcut looks fine until the process crashes mid-run, at which point there's no record of partial progress at all, and the whole resumability property this design exists to provide silently doesn't work.
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.