Infrastructure as code stopped being a nice-to-have around the same time cloud estates outgrew what a small platform team could click through in a portal, and by 2026 it is assumed baseline skill for anyone leading delivery of a .NET system on Azure or any other cloud. Interviewers use IaC questions to probe judgment that only shows up after you have lived through a botched state file, a policy that silently blocked a production deployment, or a drift incident nobody noticed for a month — trivia about a specific CLI flag tells them nothing that matters. For engineers with a decade or more of experience, the bar is choosing the right tool for the constraint in front of you, designing modules and pipelines that survive dozens of contributors, and knowing exactly where secrets, policy and testing fit into a change that will run unattended in production. The ten questions below cover Bicep, Terraform and Pulumi, state management, modules, drift, policy as code, environment promotion, secrets and testing infrastructure like the software it is.

Q1 How do Bicep, Terraform and Pulumi differ, and how would you choose between them for a .NET-heavy Azure organization?#

Short answer: Bicep is a free, Azure-only domain-specific language that compiles to ARM JSON and needs no state file because Azure Resource Manager is itself the source of truth. Terraform is a mature, cloud-agnostic tool that uses declarative HCL, has the largest provider ecosystem, and requires you to manage a state file yourself. Pulumi lets you describe infrastructure in a real programming language, including C#, while still using a resource engine that diffs against a state snapshot behind the scenes.

All three are declarative at the resource level — you describe the end state you want, and the tool works out how to reach it — but they diverge sharply in scope, state and authoring experience.

BicepTerraformPulumi
ScopeAzure onlyAny cloud with a providerAny cloud with a provider
LanguagePurpose-built DSL, compiles to ARM JSONHCL, a declarative configuration languageGeneral-purpose languages (C#, TypeScript, Python, Go, Java)
StateNone — ARM is the state storeA state file you store and lock yourselfA snapshot in Pulumi Cloud or a self-managed backend
Day-0 resource supportImmediate, since it compiles straight to ARMDepends on the AzureRM provider catching upDepends on the Azure provider catching up
Testingwhat-if, ARM deployment validationNative terraform test, Terratest, tflintOrdinary unit tests in the host language

The same storage account looks like this in each tool:

Bicep
resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
}
HCL
resource "azurerm_storage_account" "main" {
  name                     = var.storage_account_name
  resource_group_name      = azurerm_resource_group.main.name
  location                 = azurerm_resource_group.main.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}
C#
var storage = new StorageAccount("storage", new StorageAccountArgs
{
    ResourceGroupName = resourceGroup.Name,
    Sku = new SkuArgs { Name = SkuName.Standard_LRS },
    Kind = Kind.StorageV2,
});

Pick Bicep for Azure-only estates that want the lowest operational overhead and same-day support for new resource types. Pick Terraform when you need one consistent tool across multiple clouds, when the organization already has Terraform expertise, or when you want the broadest third-party module ecosystem. Pick Pulumi when the team wants full language tooling — loops, classes, package management, unit tests with the same frameworks used for application code — or is already strongest in C#. In practice, many .NET-heavy Azure shops run Bicep for Azure resources and reach for Terraform only where a workload spans a non-Azure service that has a Terraform provider but no Bicep equivalent, mixing tools by boundary rather than replacing one wholesale with the other.

What interviewers look for: concrete trade-offs instead of marketing language, and an answer that treats the choice as organizational — existing skills, multi-cloud need, governance model — as much as technical.

Common mistakes:

  • Treating the choice as a religious debate rather than matching it to real constraints.
  • Not knowing that Bicep needs no state file at all, a frequent gap for candidates who have only used Terraform.
  • Dismissing Pulumi as "not really IaC" because it uses a general-purpose language; it still produces a declarative resource graph and a plan/diff step before anything is applied.

Q2 How does Terraform's state file work, and what typically goes wrong when a team manages it carelessly?#

Short answer: Terraform state is a JSON document that maps every resource block in your configuration to the real-world object Terraform created for it, plus enough metadata to compute a diff on the next plan. Because that file is both the source of truth and the lock target for concurrent runs, teams that leave it on a laptop or in unlocked storage run into lost state, corrupted concurrent writes, and resources Terraform effectively "forgets" it owns.

In production, state belongs in a remote backend with locking: an azurerm backend using a storage account container with blob lease locking, an S3 bucket paired with a DynamoDB lock table, or a managed option such as HCP Terraform workspaces.

HCL
terraform {
  backend "azurerm" {
    resource_group_name  = "platform-tfstate-rg"
    storage_account_name = "platformtfstate"
    container_name       = "tfstate"
    key                  = "payments/prod.tfstate"
  }
}

State also holds every attribute of every managed resource, which routinely includes values you would never want committed to source control — connection strings, generated passwords, certificate material — in plaintext unless the provider explicitly writes them elsewhere. That is one reason state should never live in a repository, only in a backend with encryption at rest and access control. The second common failure is isolation: a single state file shared across many independently deployed services means an unrelated team's change can block, conflict with, or slow down yours, and a mistake in one part of the graph raises the blast radius for everyone sharing that state. The toolkit for fixing state problems without destroying real resources is terraform state list, terraform state mv, terraform import and terraform state rm — all of them re-associate Terraform's bookkeeping with reality rather than touching the underlying cloud resources.

What interviewers look for: an understanding that state is not just a cache but the map Terraform relies on to know what it owns, plus specific remote-backend and locking knowledge instead of a vague "just use remote state."

Common mistakes:

  • Committing state files to source control, which routinely leaks secrets and guarantees merge conflicts.
  • Sharing one state file across too many independently deployed components.
  • Hand-editing the state JSON instead of using the terraform state subcommands or import.

Follow-up questions:

  • How would you split an overly large state file without downtime?
  • What happens if two engineers run apply at the same time against unlocked state?

Q3 How do you design IaC modules so they stay reusable without turning into an unmaintainable abstraction layer?#

Short answer: Treat a module like a public library API: a small, well-named set of required inputs with sane defaults for everything else, one clear responsibility rather than "everything an app needs," and outputs that expose only what a consumer actually has to wire into the next layer. Version modules explicitly and let call sites pin a version instead of always tracking a module's latest commit.

The most common way modules go wrong is trying to parametrize every possible variant into one "god module" with dozens of optional booleans — feature-flag explosion that makes the module harder to read than the duplication it was meant to remove. A useful discipline is the "three strikes" rule: do not extract a module until you have written the same block of resources three times for real, because extracting after one or two uses usually means guessing the wrong interface. Both major registries support semantic versioning at the call site:

Bicep
module appService 'br:myregistry.azurecr.io/bicep/modules/app-service:1.4.0' = {
  name: 'checkoutAppService'
  params: {
    appServiceName: 'checkout-api'
    skuName: 'P1v3'
  }
}
HCL
module "app_service" {
  source  = "app.terraform.io/contoso/app-service/azurerm"
  version = "~> 2.3"

  name     = "checkout-api"
  sku_name = "P1v3"
}

Bicep resolves br: references against a public or private registry, typically an Azure Container Registry holding modules as OCI artifacts; Terraform resolves versioned sources against the public Terraform Registry or a private one. Either way, pinning a version means a module maintainer can change main without silently changing every consumer's next apply. Test each module in isolation with its own example or fixture that can be planned on its own, so a change to a shared module is validated before it ripples out to every service that consumes it.

What interviewers look for: judgment about when not to abstract, concrete registry and versioning mechanics, and awareness that a badly designed module creates more toil than the duplication it removed.

Common mistakes:

  • Building one mega-module with dozens of optional parameters instead of several small, composable ones.
  • Never pinning module versions, so every apply can silently pull in an unreviewed change.

Q4 What is configuration drift, and how do you detect and remediate it before it causes an incident?#

Short answer: Drift is any gap between what your IaC configuration says should exist and what is actually running, usually caused by a manual portal change, an emergency hotfix applied directly to a resource, or another automation touching the same resources. Left undetected, it means your next routine apply can silently revert someone's manual fix, or your documented infrastructure simply stops matching reality at the exact moment an incident responder needs it to be accurate.

Detection mechanics differ by tool. terraform plan refreshes real resource state by default before computing its diff, and terraform plan -refresh-only isolates that comparison so you can review drift without proposing any changes. For ARM and Bicep, az deployment group what-if shows the same kind of comparison, and deployment stacks go further by associating a defined set of resources with a deployment so resources that fall outside the template can be flagged rather than left invisible. For Pulumi, pulumi refresh updates state to match reality, and a subsequent pulumi preview shows the resulting delta.

Detection alone is not a strategy. Pair it with a scheduled, plan-only pipeline that runs nightly and posts drift to a channel without applying anything; least-privilege access so fewer people can make manual changes in the first place; deny-by-policy for direct writes to protected resource groups; and a clear cultural rule that code wins — any manual emergency change gets back-ported into the IaC source within a fixed window, or the next apply will revert it deliberately, not by accident.

What interviewers look for: the specific command or mechanism per tool, not just "run a diff," and a process answer, since detection without a remediation and prevention loop just restates the problem.

Common mistakes:

  • Only detecting drift reactively during an incident instead of on a schedule.
  • Enforcing "code wins" so rigidly that a genuine emergency fix gets silently reverted, causing a second incident on top of the first.

Follow-up questions:

  • How would you stop an on-call engineer's emergency portal fix from being wiped out by the next pipeline run?

Q5 What is policy as code, and how would you enforce it consistently across many Azure subscriptions?#

Short answer: Policy as code expresses an organization's compliance and governance rules — allowed regions, required tags, mandatory encryption, no publicly exposed storage — as machine-checked rules that run automatically against every deployment, instead of a wiki page engineers are trusted to remember. On Azure that is primarily Azure Policy assigned at management-group scope, complemented by static scanners such as Checkov or tfsec running in CI for Terraform.

Azure Policy has four effects worth knowing precisely: Deny blocks a non-compliant deployment outright; Audit/AuditIfNotExists flags non-compliant resources without blocking anything; Modify alters properties such as tags in flight; and DeployIfNotExists auto-remediates by deploying a related resource, such as attaching a missing diagnostic setting. Policies are usually assigned at management group scope so every subscription underneath inherits them, and related rules are bundled into initiatives so a whole regulatory baseline can be assigned as one unit.

JSON
{
  "properties": {
    "displayName": "Deny public network access on storage accounts",
    "policyType": "Custom",
    "mode": "Indexed",
    "policyRule": {
      "if": {
        "field": "Microsoft.Storage/storageAccounts/publicNetworkAccess",
        "notEquals": "Disabled"
      },
      "then": { "effect": "deny" }
    }
  }
}

The stronger version of this answer shifts policy left: run the same or an equivalent check at PR time with a static scanner against the Bicep or Terraform source, so a developer learns about a violation in seconds rather than after a ten-minute pipeline run hits a Deny policy at apply time. A what-if run combined with policy evaluation, or a linter with custom rules, catches some violations even earlier, at authoring time in the editor.

What interviewers look for: a clean distinction between preventative (Deny), detective (Audit) and corrective (DeployIfNotExists) controls, and a shift-left instinct that catches violations in the PR rather than only at deployment time.

Common mistakes:

  • Relying solely on a Deny policy at deployment time as the only guardrail.
  • Writing policies so broad they block legitimate exceptions, pushing teams toward blanket exemptions that quietly defeat the policy's purpose.

Q6 How do you promote infrastructure changes safely from development through staging to production?#

Short answer: Use the same artifact and the same computed plan through every environment, parameterizing only environment-specific values such as region, SKU or scaling limits rather than maintaining separate templates per environment. Gate each promotion behind a reviewed plan or what-if output and, for production, a human approval, and keep environments as close to identical in topology as budget allows so a passing staging run is a trustworthy predictor of production.

The detail candidates often miss is storing the plan itself as a pipeline artifact between the plan and apply stages, so what gets approved is exactly what gets applied, not a fresh re-plan that could have drifted in the interim.

YAML
jobs:
  plan:
    steps:
      - run: terraform plan -var-file=envs/prod.tfvars -out=prod.tfplan
      - uses: actions/upload-artifact@v4
        with:
          name: prod-tfplan
          path: prod.tfplan

  apply:
    needs: plan
    environment: production
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: prod-tfplan
      - run: terraform apply prod.tfplan

On the promotion model, prefer promoting one reviewed commit through environment-specific pipeline stages over Terraform workspaces selected by the operator at run time. Workspaces work, but selecting the wrong workspace before an apply is a common, high-blast-radius mistake; a directory or pipeline stage per environment makes the target explicit in the pipeline definition rather than in operator memory. The same principle applies to Bicep parameter files: one file per environment, referenced explicitly by the pipeline stage that deploys it.

What interviewers look for: the "plan as an artifact, not re-computed" detail, a real approval-gate mechanism, and awareness of the workspace-selection failure mode.

Common mistakes:

  • Re-running plan immediately before apply instead of applying the exact reviewed plan file.
  • Keeping environments so different from production — a smaller topology, a different auth model — that a green staging run gives false confidence.

Q7 How should secrets be handled through an IaC pipeline, from source control to the deployed resource?#

Short answer: Secrets should never exist as plaintext in source control or in a variables file; the pipeline should pull them at plan or apply time from a dedicated store such as Azure Key Vault, and the deployed resource itself should authenticate to Key Vault at runtime through a managed identity rather than receiving the secret value baked into its configuration.

The mechanics differ enough to matter in an interview. In Bicep, mark parameters @secure() so the value is never written to the deployment history or logs the way an ordinary parameter is:

Bicep
@secure()
param sqlAdminPassword string

resource sqlServer 'Microsoft.Sql/servers@2023-08-01' = {
  name: sqlServerName
  location: location
  properties: {
    administratorLogin: 'sqladmin'
    administratorLoginPassword: sqlAdminPassword
  }
}

In Terraform, a variable marked sensitive = true is redacted from CLI output and plan diffs, but that flag does not encrypt the value inside the state file — it still lands there in plaintext unless the resource that ultimately needs it fetches the value itself at runtime instead of receiving it through a variable. Pulumi's secrets are meaningfully different by default: a value explicitly marked as a Pulumi secret is encrypted inside the state snapshot using the backend's own encryption provider, not merely hidden from output.

The strongest end state, regardless of tool, is that IaC never carries the secret value through the pipeline at all: it provisions the vault, the access policy or RBAC role assignment, and the application's managed identity, and the running application fetches the secret itself from Key Vault at startup. That removes an entire class of leakage risk — no pipeline log, plan file or state snapshot ever contains the value in the first place.

What interviewers look for: the nuance that sensitive = true in Terraform only redacts output rather than encrypting state, and the judgment to prefer runtime retrieval via managed identity over injecting secret values through the pipeline at all.

Common mistakes:

  • Believing sensitive = true protects a value inside the state file.
  • Storing secrets "temporarily" in a .tfvars or parameters file that later gets committed by accident.

Follow-up questions:

  • How would you rotate a database credential referenced by ten different services' IaC without downtime?

Q8 How do you test infrastructure code before it ever reaches a shared environment?#

Short answer: Layer it the way you would layer application tests: static and policy scanners on every commit that need no cloud calls, a plan or what-if review that acts as a unit test of the diff, a native or third-party framework that asserts on planned or applied output, and a scheduled, disposable apply-then-destroy run against a real environment reserved for the modules that matter most.

Static analysis tools such as tflint, Checkov and tfsec catch an open security group, an unencrypted storage account, or a missing required tag before anything talks to a cloud API, and they run in seconds on every pull request. Terraform's native terraform test command, generally available since Terraform 1.6, lets you assert on plan or apply output directly in HCL using .tftest.hcl files:

HCL
run "storage_account_enforces_tls12" {
  command = plan

  assert {
    condition     = azurerm_storage_account.main.min_tls_version == "TLS1_2"
    error_message = "Storage account must enforce TLS 1.2 minimum"
  }
}

For Bicep, az deployment group what-if plus a diff assertion in a pipeline script, or a linter such as PSRule for Azure, plays a similar role. For Pulumi, the infrastructure program is ordinary code, so you write unit tests in the host language's own framework and use Pulumi's mocking APIs to assert on resource properties without calling a cloud provider at all. For end-to-end confidence, Terratest spins up real infrastructure in a throwaway resource group, runs assertions against the live resources, and tears everything down — valuable for a handful of critical modules, too slow and expensive to run on every commit for everything.

What interviewers look for: a layered strategy mirroring application test pyramids rather than "we eyeball the plan output," plus specific tool names per language.

Common mistakes:

  • Treating a clean plan as sufficient proof of correctness; a plan can succeed while still creating a resource with the wrong configuration if no assertion checks for it.
  • Running full apply-and-destroy integration tests on every commit, which is slow and leaves account clutter whenever a teardown step itself fails.

Q9 You inherit a Terraform codebase where forty microservices share one state file. Walk through how you would fix it.#

Short answer: Split the monolithic state along the boundaries the services already have — one state per service or bounded context — using terraform state mv or, for a cleaner cut, terraform import into a new state paired with terraform state rm from the old one, so Terraform re-associates existing real resources with new state files without destroying and recreating anything.

The concrete sequence matters more than the general idea. First, inventory which resources belong to which service using tags or naming, not guesswork, since a shared state file is often shared precisely because nobody has ever mapped ownership cleanly. Second, pick one low-risk service as a pilot: migrate its resources into a dedicated state file or backend key, then verify with terraform plan that the new state shows zero changes before moving on. Third, for any resource that resists a clean move — renamed during the split, or genuinely ambiguous ownership — fall back to import against the new state and state rm against the old one, again confirming a clean plan on both sides before deleting anything. Fourth, repoint that service's pipeline at its new backend key and remove its resources from the shared configuration only once its own plan is confirmed clean, never before. Finally, once every service has migrated, decommission the shared state so future services default to per-service ownership from day one.

Throughout, never delete anything from the old state until plan against the new state shows zero drift, and migrate incrementally rather than as one cutover — a big-bang migration of forty services at once multiplies the blast radius of any single mistake to all forty simultaneously.

What interviewers look for: knowledge of the actual mechanics (state mv, import, state rm) rather than "I'd just recreate everything," plus the operational judgment to migrate incrementally and verify with plan before deleting anything.

Common mistakes:

  • Deleting and recreating resources instead of migrating state, causing real downtime for a purely organizational change.
  • Attempting a single big-bang cutover of all forty services instead of proving the process on one low-risk service first.

Q10 What does "idempotent" actually mean for an IaC tool, and how do you recover from a deployment that failed halfway through?#

Short answer: Idempotent means running the same configuration against the same starting state always converges on the same end state, no matter how many times you run it or whether a prior run partially succeeded. That is what lets you simply re-run apply, or resubmit an ARM deployment, after a partial failure instead of having to manually work out and undo whatever did complete.

The mechanism is a diff, not a script: each run computes the difference between desired and current state and only acts on that difference, so resources already correctly created on a failed prior run are left alone and the tool continues with what remains. Contrast that with an imperative sequence of CLI calls with no built-in notion of current state, where re-running after a partial failure risks creating duplicates or erroring on a resource that already exists, unless the script author has hand-written idempotency checks at every step.

Three nuances separate a senior answer from a textbook one. A failed apply can leave Terraform's state slightly out of sync with reality if the failure happens between a provider creating a resource and Terraform recording it, which is exactly why a plan or refresh before the retry matters more than blindly re-running apply. ARM and Bicep deployments are tracked server-side with deployment history, so redeploying the same template is naturally idempotent for anything expressed declaratively, but an imperative step outside the template — a deployment script resource, a post-deployment pipeline step — needs its own idempotency guarantee, because that guarantee stops at the boundary of what the template actually manages. And an order-of-operations failure, where a dependent resource is created before its dependency finishes provisioning, almost always indicates a missing explicit dependency declaration rather than a tooling bug — the fix is to declare the dependency, not to wrap the command in a retry loop.

What interviewers look for: a precise definition, not "it means it's safe to run twice," plus the judgment that idempotency does not cover provider-level bugs or imperative steps living outside the declarative model.

Common mistakes:

  • Assuming idempotency means zero risk, when a failed run can leave state slightly stale until the next plan or refresh reconciles it.
  • Adding manual retry or sleep loops around IaC commands instead of fixing the missing dependency declaration that caused the ordering failure.

Quick-Fire Round#

QuestionAnswer
Does Bicep need a state file?No — Azure Resource Manager is the state store.
Which Terraform flag previews drift without changing anything?-refresh-only on terraform plan.
Which Terraform release made terraform test generally available?Terraform 1.6.
Which Azure Policy effect blocks a non-compliant deployment outright?Deny.
Does sensitive = true encrypt a Terraform variable inside state?No — it only redacts CLI and log output.
Which command safely re-associates a resource with a new state file?terraform state mv.
Which tool encrypts secret values inside its state by default?Pulumi.
Which decorator marks a Bicep parameter as secure?@secure().

How to Prepare#

  • Stand up the same resource — a storage account or web app — in Bicep, Terraform and Pulumi with C#, so you can speak to real syntax differences instead of secondhand comparisons.
  • Deliberately split a local Terraform state file in a throwaway project, then practice state mv, import and state rm until the sequence is second nature.
  • Write one .tftest.hcl test and one Pulumi unit test against a real module so you can describe the testing story from experience, not theory.
  • Wire up a Key Vault-backed secret end to end — IaC provisioning the vault and role assignment, the app fetching the secret at runtime through a managed identity — and be ready to explain why that beats passing a secret value through the pipeline.
  • Practice describing a real drift or state incident you have handled, including what you would change about the pipeline to prevent a repeat.