CoreHRx Jobs

Apply in one click, save jobs and take free mock tests.

Register free Log in
Hiring? Post jobs with CoreHRx
My account
user

View & update profile
Interview guide

DevOps Interview Questions and Answers: 60 Questions from Easy to Hard

This guide is for freshers and experienced engineers in India preparing for DevOps, build and release, cloud and SRE interviews. It has 60 questions in three levels. Easy covers what every fresher is asked: CI/CD basics, the structure of Jenkins, GitHub Actions, GitLab CI and Azure Pipelines files, Terraform and Ansible fundamentals, release strategies and SLOs. Medium covers daily work: faster pipelines, approvals, state locking, idempotent playbooks, alerts and error budgets. Hard covers senior topics: monorepo pipelines, canary analysis, Terraform refactoring at scale, supply-chain security, burn-rate alerting and incident handling. Docker, Kubernetes and Git have their own guides. Answer each question aloud first, then compare, and finish with the mock test.

What DevOps interviews cover

  • CI/CD pipelines: stages, caching, artifacts and approvals in Jenkins, GitHub Actions, GitLab CI and Azure Pipelines, often with a pipeline file to read or fix.
  • Release strategies: blue-green, canary and rolling deployments, feature flags, rollback and database changes without downtime.
  • Infrastructure as code and configuration management: Terraform state, plans and modules, and Ansible playbooks, handlers, roles and idempotency.
  • Reliability: monitoring and alerting, SLIs, SLOs and error budgets, incident handling and the four DORA metrics.
  • Security in the pipeline: handling secrets, short-lived cloud credentials, dependency and code scanning, and supply-chain risks.

How to prepare (freshers and experienced)

  • Freshers: build one small project end to end, such as a Node or Python app with a GitHub Actions pipeline that tests it and deploys it, and be ready to explain every line of the YAML.
  • Learn enough Linux and bash to read logs, check processes and ports, and write a safe script with set -euo pipefail; most DevOps rounds include a shell question.
  • Practise Terraform and Ansible on a free-tier cloud account: create, change and destroy something, and read every plan before you apply it. Destroy what you create so you are not billed.
  • Experienced candidates: prepare two or three real stories, such as a pipeline you made faster, an incident you led or a move to infrastructure as code, with before and after numbers.
  • Revise the SRE arithmetic: error budgets, burn rates and combined availability come up in senior rounds, and interviewers expect you to work them out aloud.

Common mistakes and interview-day tips

  • Listing tools without the reason for them. Say what problem a tool solved, such as cutting deploy time from an hour to ten minutes, not only that you used it.
  • Saying you would fix production by logging in and editing files by hand. Interviewers want the change made through code, review and the pipeline, with a rollback plan.
  • Forgetting state and data: rollback, Terraform state and database migrations are where many real outages start, so mention them in design answers.
  • Writing a password into a pipeline file on the whiteboard. Say where the secret is stored and how it reaches the job instead.
  • When asked to design a pipeline, start with the requirements, such as how often the team releases and how fast a rollback must be, before naming tools.

DevOps interview questions: Easy questions (freshers)

20 questions

1. What is DevOps, and what problem does it solve?

DevOps is a way of working in which development and operations share responsibility for building, releasing and running software, backed by automation. It fixes the old hand-off problem: developers passed code over the wall, operations deployed it rarely and by hand, and every failure led to blame. In DevOps the same team owns a service end to end, using version control, automated tests, CI/CD, infrastructure as code and production monitoring. It is a culture and a set of practices, not a job title or a tool; installing Jenkins does not make a team DevOps.

2. What is the difference between continuous integration, continuous delivery and continuous deployment?

Continuous integration means developers merge small changes into the main branch often, at least daily, and every merge triggers an automated build and test run, so integration problems show up within minutes. Continuous delivery goes further: every change that passes the pipeline is kept releasable, and going to production is a one-click business decision. Continuous deployment removes that manual step, so every passing change reaches production automatically. A common mistake is calling a nightly build CI; if branches live for weeks, the team is not integrating continuously.

3. What are the typical stages of a CI/CD pipeline?

A common order is: check out the code, build it, run unit tests, run static checks such as lint and security scans, package the result as a versioned artifact or container image, deploy it to a test or staging environment, run integration or smoke tests there, and finally deploy to production, with an approval gate if the team practises continuous delivery. Fast, cheap checks come first so a broken build fails in two minutes, not after a 20-minute deploy. Each stage should stop the pipeline when it fails instead of carrying on.

4. What is a Jenkinsfile, and what does a basic declarative pipeline look like?

A Jenkinsfile is a text file in the root of the repository that defines the pipeline as code, so it is versioned and reviewed like the application. In declarative syntax everything sits inside a pipeline block: agent says where to run, stages holds named stage blocks, each stage has steps such as sh to run shell commands, and post runs actions after the stages depending on the result, such as always, success or failure. Keeping the pipeline in the repository beats building jobs by clicking in the web UI, which nobody can review or roll back.

Example
pipeline {
    agent any
    stages {
        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }
        stage('Test') {
            steps {
                sh 'npm test'
            }
        }
    }
    post {
        failure {
            echo 'Build failed, check the console log'
        }
    }
}

5. In GitHub Actions, what are a workflow, an event, a job, a step and a runner?

A workflow is a YAML file in .github/workflows that GitHub runs automatically. An event such as push or pull_request triggers it, set under on. A workflow has one or more jobs, which run in parallel by default, each on a fresh runner: a virtual machine such as ubuntu-latest hosted by GitHub, or your own self-hosted machine. Inside a job, steps run in order on the same runner; a step either runs a shell command with run or uses a ready-made action with uses. Steps in a job share files, but separate jobs do not unless you pass artifacts.

Example
name: ci
on:
  push:
    branches: [main]
  pull_request:
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test

6. How does a .gitlab-ci.yml file define stages and jobs?

The file lives in the repository root. stages lists the stage names in order, and every other top-level key that is not a reserved keyword is a job. Each job names its stage and has a script, the shell commands to run, often inside a Docker image set with image. Jobs in the same stage run in parallel, and by default the next stage starts only when all jobs in the previous one succeed. Here deploy-staging runs only on the main branch because of its rules. A frequent mistake is assuming files built in one job exist in the next; they do not unless you declare artifacts.

Example
stages:
  - build
  - test
  - deploy

build-app:
  stage: build
  image: node:20
  script:
    - npm ci
    - npm run build

unit-tests:
  stage: test
  image: node:20
  script:
    - npm ci
    - npm test

deploy-staging:
  stage: deploy
  script:
    - ./deploy.sh staging
  environment: staging
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

7. What is the structure of an Azure Pipelines YAML file?

In Azure DevOps, a YAML pipeline is usually a file named azure-pipelines.yml in the repository. trigger sets which branch pushes start a run, and pool picks the agent, here a Microsoft-hosted Ubuntu image. The hierarchy is stages, then jobs, then steps: stages are large phases such as Build and Deploy, a job runs on one agent, and steps are tasks or scripts that run in order inside it. For a simple pipeline you can leave out stages and list jobs, or even steps, directly. Classic pipelines made in the web editor still exist, but YAML keeps the pipeline in version control.

Example
trigger:
  branches:
    include:
      - main

pool:
  vmImage: ubuntu-latest

stages:
  - stage: Build
    jobs:
      - job: BuildAndTest
        steps:
          - script: npm ci
            displayName: Install packages
          - script: npm test
            displayName: Run tests

8. What is infrastructure as code, and how do the declarative and imperative styles differ?

Infrastructure as code means servers, networks, databases and permissions are described in files kept in Git, and a tool creates or changes them, instead of someone clicking in a cloud console. You get review through pull requests, a full history, repeatable environments and quick rebuilds after a disaster. In the declarative style, used by Terraform, you describe the end state, such as three servers, and the tool works out what to add or remove. In the imperative style, like a plain shell script, you write the steps yourself, so running it twice may create six servers.

9. What does each command in the basic Terraform workflow do?

terraform init prepares the working directory: it downloads providers and modules and connects to the state backend. fmt rewrites files in the standard style, and validate checks syntax and internal references without calling any cloud API. plan compares the code with the state and the real resources and shows what will be created, changed or destroyed; saving it with -out lets apply run exactly that plan. apply makes the changes and updates the state. destroy deletes everything the configuration manages, so treat it with care. Always read the plan, especially lines that say destroy.

Example
terraform init
terraform fmt
terraform validate
terraform plan -out=tfplan
terraform apply tfplan
terraform destroy

10. What is the Terraform state file, and why must it be handled carefully?

The state file, terraform.tfstate by default, maps each resource in your code to the real object in the cloud, such as the ID of a virtual machine, and stores its last known attributes. Terraform uses it to work out what a plan must change. If it is lost, Terraform no longer knows it manages those objects and will try to create them again. It can also hold secrets in plain text, such as database passwords, so never commit it to Git. Teams keep it in a remote backend, such as an S3 bucket or Azure storage, with locking and encryption, and change it only through Terraform.

11. What is Ansible, and what are an inventory and a playbook?

Ansible is an automation tool for configuring servers and deploying software. It is agentless: it connects over SSH, or WinRM for Windows, and runs small modules on the target, so Linux hosts need only Python, not an installed agent. The inventory lists hosts and groups, such as a web group of three servers in Mumbai. A playbook is a YAML file of plays; each play maps a host group to a list of tasks, and each task calls a module. Here the apt module makes sure nginx is installed and the service module keeps it running and enabled at boot.

Example
- name: Set up web servers
  hosts: web
  become: true
  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Make sure nginx is running
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

12. What is configuration drift, and how do mutable and immutable infrastructure deal with it?

Configuration drift is when servers that should be identical slowly become different, because someone patched one by hand, edited a config file on another, or a script failed halfway. Drift causes bugs that appear on one server only. With mutable infrastructure you keep servers running, and a tool such as Ansible or Puppet regularly re-applies the desired configuration. With immutable infrastructure you never change a running server: you build a new image with the change and replace the old servers. Immutable is more predictable but needs fast automated builds.

13. What is a blue-green deployment?

In a blue-green deployment you run two identical production environments. Blue serves all users while you deploy the new version to green and test it there; then you switch the load balancer so traffic goes to green. If something breaks, you switch back to blue, which still runs the old version, so rollback takes seconds. The costs are double infrastructure during the release and care with shared parts such as the database, because both versions must work with the same schema. Switching through DNS instead is slower and less predictable, because clients cache the old record.

14. What is a canary release, and how is it different from blue-green?

A canary release sends a small share of real traffic, say 5 percent, to the new version while the rest stays on the old one, and you compare error rates, latency and business metrics between the two groups. If the canary looks healthy, you raise the share step by step, for example 5, 25, 50 and then 100 percent; if not, you send all traffic back to the old version. Blue-green moves everyone at once, so a bug reaches all users until you switch back, while a canary limits the damage to a small group. The catch is that a canary needs good monitoring, or you cannot judge it.

15. What is a rolling deployment, and how do you roll back a bad release?

A rolling deployment replaces instances of the old version a few at a time. With ten servers behind a load balancer you might take two out, update them, check their health and put them back, then move to the next two. It needs no extra environment, but for a while both versions serve users, so they must be compatible. The quickest safe rollback is redeploying the previous artifact version, which is why every build should be versioned and kept. Avoid rushing an untested hotfix under pressure, and plan how to undo database changes, because redeploying old code does not reverse a migration.

16. What are metrics, logs and traces, and when would you use each?

Metrics are numbers measured over time, such as requests per second, error rate or CPU use. They are cheap to store, ideal for alerts, and tell you that something is wrong. Logs are timestamped records of individual events, such as an error with its stack trace, and tell you what exactly happened. Traces follow one request across several services and show where the time went, for example 900 ms waiting on a payment service. Together they are called the three pillars of observability. A typical flow: an alert on a metric, a trace to find the slow service, then its logs for the cause.

17. What is the difference between an SLI, an SLO and an SLA?

An SLI, service level indicator, is a measurement of how the service behaves for users, such as the percentage of requests that succeed or that return within 300 ms. An SLO, service level objective, is the target the team sets for that SLI, for example 99.9 percent of requests succeeding over 30 days. An SLA, service level agreement, is a contract with customers that states the consequences, such as service credits, if a promised level is missed. The SLA should be looser than the internal SLO, so the team is warned and can react before the business owes anyone money.

18. What are the four DORA metrics?

DORA, the DevOps Research and Assessment programme, identified four measures of software delivery performance. Deployment frequency is how often you release to production. Lead time for changes is the time from a commit to that commit running in production. Change failure rate is the share of deployments that cause a failure needing a rollback or hotfix. Time to restore service is how long it takes to recover from such a failure. The first two measure speed and the last two stability, and the research found that strong teams do well on both together, so speed and stability are not a trade-off.

19. How should a CI/CD pipeline handle secrets such as API keys?

Secrets never go into the repository, the pipeline file or a container image. Keep them in the CI tool's secret store, such as GitHub Actions secrets, masked and protected GitLab CI/CD variables or Azure DevOps variable groups, or in a vault such as HashiCorp Vault or a cloud secrets manager. Inject them as environment variables only into the step that needs them, as here, and scope production secrets to a protected environment or branch. Log masking helps but is not a guarantee: a transformed value, such as a base64-encoded secret, is not recognised, so never print secrets while debugging.

Example
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        run: ./deploy.sh
        env:
          PAYMENT_API_KEY: ${{ secrets.PAYMENT_API_KEY }}

20. How does a pipeline decide that a step failed? What does this script print?

Every command returns an exit code: 0 means success and any value from 1 to 255 means failure. CI tools such as Jenkins, GitHub Actions and GitLab CI check the code of each step, and a non-zero result marks the step failed and normally stops the job. This script prints exit code: 3, then false failed with 1, then true passed with 0, because $? holds the status of the last command, || runs its right side only on failure and && only on success. A classic bug is a build script without error handling: an early command fails, the last one succeeds, and the step goes green.

Example
#!/bin/bash
check_disk() {
    return 3
}
check_disk
echo "exit code: $?"
false || echo "false failed with $?"
true && echo "true passed with $?"

Ready to check yourself? Take the free DevOps mock test

DevOps interview questions: Medium questions (1-3 years)

20 questions

21. Your team's pipeline takes 25 minutes. How would you make it faster?

First measure: look at the time each stage takes and fix the slowest, instead of guessing. The usual wins are caching dependencies so npm or Maven packages are not downloaded on every run; splitting tests into parallel shards, as here, where four runners each run a quarter of the Jest tests; running cheap checks such as lint first so failures come early; and skipping work a change cannot affect, with path filters. Reusing Docker layers helps too. A trap is a cache with a careless key, which serves stale or mismatched files; key it on the lock file, as setup-node does here.

Example
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npx jest --shard=${{ matrix.shard }}/4

22. What does build once, deploy many mean, and why should you not rebuild for each environment?

It means the pipeline builds the application once, gives the result a unique version, such as 2.4.1 or the Git commit SHA, stores it in an artifact repository or container registry, and then deploys that same artifact to test, staging and production. Only configuration, such as database URLs, changes between environments, and it comes from environment variables or a config service. If you rebuild for production, you ship a binary that was never tested: a dependency may have changed in between, or the build may use other flags. Moving tags such as latest are risky for the same reason.

23. How do you test against several versions or operating systems in GitHub Actions?

Use a matrix strategy. GitHub creates one job for each combination of matrix values: here 2 operating systems times 2 Python versions is 4, and exclude removes one, leaving 3 jobs that run in parallel. Each job reads its values through the matrix context, such as matrix.python. fail-fast: false keeps the other jobs running when one fails, so you see every broken combination at once; the default, true, cancels the rest. Always quote version numbers: unquoted 3.10 is read by YAML as the number 3.1, a classic bug that installs the wrong Python.

Example
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
        python: ['3.11', '3.12']
        exclude:
          - os: windows-latest
            python: '3.11'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python }}
      - run: pip install -r requirements.txt
      - run: pytest

24. How do jobs in a GitHub Actions workflow depend on each other and pass data?

Jobs run in parallel on separate runners unless you add needs, which makes a job wait for the listed jobs and skips it if any of them fails. Because each job gets a fresh machine, files do not carry over: upload them with upload-artifact in one job and fetch them with download-artifact in the next. For small values, such as a version string, a step appends name=value to the file named by GITHUB_OUTPUT, the job maps it under outputs, and later jobs read it through the needs context. The old set-output command is deprecated, so new workflows should use GITHUB_OUTPUT.

Example
jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.ver.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      - id: ver
        run: echo "version=1.4.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
      - run: make dist
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - run: ./deploy.sh "${{ needs.build.outputs.version }}"

25. How do you use credentials safely in a Jenkins pipeline?

Store the secret in the Jenkins credentials store, never in the Jenkinsfile, and bind it only where it is needed with withCredentials. The binding puts the values into environment variables for that block and masks them in the console log. Note the single quotes around the sh command: the shell expands $NEXUS_PASS, so the secret never passes through Groovy. With double quotes, Groovy would put the password into the command string itself, which Jenkins warns against. Also run builds on agents, not on the controller, so a build script cannot read the controller's files and stored credentials.

Example
pipeline {
    agent { label 'linux' }
    environment {
        REPO_URL = 'https://nexus.example.in/repository/releases/'
    }
    stages {
        stage('Publish') {
            steps {
                withCredentials([usernamePassword(
                        credentialsId: 'nexus-deploy',
                        usernameVariable: 'NEXUS_USER',
                        passwordVariable: 'NEXUS_PASS')]) {
                    sh 'curl -fsS -u "$NEXUS_USER:$NEXUS_PASS" -T app.jar "$REPO_URL"'
                }
            }
        }
    }
}

26. How do you run stages in parallel and add a manual approval before production in a declarative Jenkins pipeline?

Put the stages that should run together inside a parallel block; Jenkins runs Unit tests and Lint at the same time, and Checks fails if either fails. For approval, a stage-level input directive pauses the run until someone clicks Proceed, and submitter limits who may approve. when with branch skips the stage on other branches in a multibranch pipeline. One detail trips people up: by default Jenkins asks for the input before it evaluates when, so beforeInput true makes it check the branch first. Also wrap approvals in a timeout, or a forgotten one keeps the run and its executor waiting.

Example
pipeline {
    agent any
    stages {
        stage('Checks') {
            parallel {
                stage('Unit tests') {
                    steps { sh 'make test' }
                }
                stage('Lint') {
                    steps { sh 'make lint' }
                }
            }
        }
        stage('Deploy to prod') {
            when {
                beforeInput true
                branch 'main'
            }
            input {
                message 'Deploy this build to production?'
                submitter 'release-managers'
            }
            steps { sh './deploy.sh prod' }
        }
    }
}

27. How do you control when a GitLab CI job runs, and how do you make a production deploy manual?

Use rules, which GitLab recommends over the older only and except keywords. GitLab checks the rules in order and the first match decides; if none match, the job is left out of the pipeline. Here the job is added only for tags that look like v1.4.2, and when: manual makes it wait until someone clicks run, so creating a tag does not deploy by itself. The final when: never just makes that default explicit. environment records each deployment, so you can see which version runs where and redeploy an older one. Protect the v tags so only maintainers can create them.

Example
deploy-prod:
  stage: deploy
  script:
    - ./deploy.sh production
  environment:
    name: production
    url: https://app.example.in
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
      when: manual
    - when: never

28. What is the difference between cache and artifacts in GitLab CI?

Cache is for speed: it stores things you could download again, such as npm or Maven dependencies, and later jobs and pipelines reuse it if it is there, with no guarantee that it is. Here the cache key comes from package-lock.json, so it changes only when dependencies change. Artifacts are build outputs that later jobs need, such as the dist folder: GitLab uploads them and passes them to jobs in later stages of the same pipeline, and you can download them from the UI until expire_in. Do not use cache to pass build output between stages, because on another runner it may be missing.

Example
build:
  stage: build
  image: node:20
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - .npm/
  script:
    - npm ci --cache .npm --prefer-offline
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week

29. How do you add approvals and environment-specific secrets to a production deployment in Azure Pipelines?

Use a deployment job that targets an environment. Approvals and checks are set on the environment itself, in the Environments section of Azure DevOps, so every pipeline that deploys to production waits for the named approvers, a business-hours window or other checks; they are not written in the YAML. Values shared across pipelines go in a variable group, which can link to Azure Key Vault, and secret variables must be mapped into a script's env explicitly, as with DB_PASSWORD here. One surprise: deployment jobs do not check out the repository by default, so add checkout: self.

Example
stages:
  - stage: DeployProd
    dependsOn: Build
    variables:
      - group: payments-prod
    jobs:
      - deployment: DeployWeb
        environment: production
        pool:
          vmImage: ubuntu-latest
        strategy:
          runOnce:
            deploy:
              steps:
                - checkout: self
                - script: ./deploy.sh
                  env:
                    DB_PASSWORD: $(DbPassword)

30. Why do teams use a remote backend with locking for Terraform state?

A local state file works for one person, but a team needs one shared copy, or two engineers apply against different versions and create duplicates or undo each other's work. A remote backend stores the state centrally, here in an S3 bucket in Mumbai, encrypted, with bucket versioning on so an old copy can be recovered. Locking stops two applies running at once: the second fails with a lock error, or waits if you pass -lock-timeout, instead of corrupting the state. From Terraform 1.11 the S3 backend can lock with a lock file next to the state, as shown; older setups used a DynamoDB table.

Example
terraform {
  backend "s3" {
    bucket       = "fintech-tfstate-mumbai"
    key          = "payments/prod/terraform.tfstate"
    region       = "ap-south-1"
    encrypt      = true
    use_lockfile = true
  }
}

31. terraform plan says a resource must be replaced. What does that mean, and how do you control it?

It means you changed an argument the provider cannot update in place, such as the AMI of an EC2 instance, so Terraform will destroy the old object and create a new one; the plan marks it -/+ and notes which attribute forces replacement. For a server that can mean downtime, and for a database it can mean data loss, so read the plan before applying. lifecycle settings help: create_before_destroy builds the new object first, ignore_changes stops Terraform reverting changes made elsewhere, such as tags added by another tool, and prevent_destroy makes any plan that would destroy the resource fail.

Example
resource "aws_instance" "api" {
  ami           = var.ami_id
  instance_type = "t3.medium"

  lifecycle {
    create_before_destroy = true
    ignore_changes        = [tags]
  }
}

32. Someone created an S3 bucket by hand in the console. How do you bring it under Terraform?

Write a resource block for it and import the existing object into state, so Terraform manages it instead of trying to create a duplicate. From Terraform 1.5 you can declare an import block, as here; it runs as part of a normal plan and apply, so the import is reviewed in a pull request like any other change. Older versions use the terraform import command, which writes to state at once. Then run plan and adjust the code until it shows no changes; otherwise Terraform will change the real bucket to match your incomplete code. plan with -generate-config-out can draft the block.

Example
import {
  to = aws_s3_bucket.reports
  id = "pune-hr-reports-prod"
}

resource "aws_s3_bucket" "reports" {
  bucket = "pune-hr-reports-prod"
}

33. What is idempotency in Ansible, and why are command and shell tasks a problem?

An idempotent task gives the same result however many times it runs: the first run makes the change, and later runs report ok because the state is already right. Most modules, such as user or apt, check before acting. command and shell cannot know what your command does, so they run every time and report changed, which hides real changes and can repeat one-time work. Prefer a real module; if you must use command, add creates or removes so it is skipped once a file exists, and changed_when: false for read-only commands. Test by running the playbook twice: the second run should show changed=0.

Example
- name: Create the app user
  ansible.builtin.user:
    name: payroll
    shell: /bin/bash

- name: Run the one-time database setup
  ansible.builtin.command: /opt/app/bin/setup-db.sh
  args:
    creates: /opt/app/.db_initialised

- name: Read the installed version
  ansible.builtin.command: /opt/app/bin/app --version
  register: app_version
  changed_when: false

34. What are handlers in Ansible, and when do they run?

A handler is a task that runs only when another task notifies it, and only if that task reported changed. The usual use is reloading a service after its config file changes, so an unchanged config causes no reload. Handlers run once, after the tasks of the play, even if several tasks notified them, and in the order they are defined, not the order of notification. If a later task fails on a host, its notified handlers are skipped by default, which can leave a new config unloaded; force_handlers or meta: flush_handlers deal with that. validate here runs nginx -t on the file first.

Example
- hosts: web
  become: true
  tasks:
    - name: Deploy nginx config
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        validate: nginx -t -c %s
      notify: Reload nginx

  handlers:
    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

35. How do you keep passwords in an Ansible project safe?

Use Ansible Vault, which encrypts whole files or single values with AES-256, so they can sit in Git safely. A common layout keeps secrets in a vault.yml next to a plain vars file in group_vars, with vault-prefixed names, so you can still search for variable names. The playbook decrypts them at run time with --ask-vault-pass or a password file, and in CI the vault password comes from the pipeline's secret store. Add no_log: true to tasks that handle secrets, or the values can appear in the output. Larger teams often fetch secrets from HashiCorp Vault or a cloud secrets manager instead.

Example
ansible-vault create group_vars/prod/vault.yml
ansible-vault edit group_vars/prod/vault.yml
ansible-vault encrypt_string 'CHANGE_ME' --name 'db_password'
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass

36. How would you calculate the DORA metrics from these deployment records for two weeks in June 2025?

Use the records the pipeline already keeps. Deployment frequency: 5 production deploys in two weeks, 2.5 a week. Lead time for changes, commit to production: 6, 26, 4, 71 and 18 hours, so the median is 18 hours; the mean, 25 hours, is pulled up by one slow release, which is why medians are preferred. Change failure rate: 2 of 5 deploys failed, 40 percent. Time to restore: 90 and 45 minutes, 67.5 minutes on average. Collect these from CI and incident tools automatically, because hand-filled spreadsheets drift, and define failure the same way every time.

Example
deploy  first_commit   in_production  failed  restored
D1      02-Jun 10:00   02-Jun 16:00   no      -
D2      03-Jun 09:00   04-Jun 11:00   yes     04-Jun 12:30
D3      05-Jun 14:00   05-Jun 18:00   no      -
D4      06-Jun 11:00   09-Jun 10:00   no      -
D5      10-Jun 15:00   11-Jun 09:00   yes     11-Jun 09:45

37. What makes a good alert, and what does the for clause in this Prometheus rule do?

A good alert fires on a symptom users feel, such as errors or slow responses, needs a human to act now, and links to a runbook. Alerts on causes, such as CPU at 85 percent, often fire while users are fine and teach people to ignore pages. This rule pages when more than 2 percent of checkout requests fail. for: 10m means the condition must stay true for 10 minutes before the alert fires; until then it is pending, so a short spike does not wake anyone at 3 a.m. Send non-urgent problems to a ticket or chat channel instead of paging, and review noisy alerts regularly.

Example
groups:
  - name: checkout-api
    rules:
      - alert: HighErrorRate
        expr: |
          sum(rate(http_requests_total{job="checkout",code=~"5.."}[5m]))
            /
          sum(rate(http_requests_total{job="checkout"}[5m])) > 0.02
        for: 10m
        labels:
          severity: page
        annotations:
          summary: Checkout 5xx rate above 2 percent for 10 minutes
          runbook_url: https://wiki.example.in/runbooks/checkout-5xx

38. What is an error budget, and how much downtime does a 99.9 percent availability SLO allow in 30 days?

The error budget is the unreliability the SLO allows: 100 percent minus the target. For 99.9 percent over 30 days it is 0.1 percent of 43,200 minutes, which is 43.2 minutes of full downtime, or the same share of failed requests if the SLI counts requests. At 99.99 percent it is only about 4.3 minutes. The team can spend the budget on risky releases, experiments and maintenance. When it is used up, a common policy is to pause feature releases and work on reliability until it recovers. This turns the old argument between speed and stability into a number both sides agree on.

Example
error budget = (1 - SLO) x period
             = (1 - 0.999) x 30 days x 24 h x 60 min
             = 0.001 x 43,200 min
             = 43.2 minutes per 30 days

39. How can a GitHub Actions workflow deploy to AWS without storing long-lived access keys?

Use OpenID Connect federation. GitHub issues a short-lived signed token for each run, AWS trusts GitHub as an identity provider, and the token is exchanged for temporary credentials, valid one hour by default here. id-token: write lets the job request the token, and the action assumes the IAM role. No access key is stored, so there is nothing to leak or rotate. The key part is the role's trust policy: restrict it to your repository and to the main branch or a production environment, or other workflows, even other repositories, could assume it. GitLab CI and Azure DevOps support the same idea.

Example
permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
          aws-region: ap-south-1
      - run: aws s3 sync ./site s3://example-site-bucket

40. Why do CI scripts start with set -euo pipefail? What does this script print?

By default bash carries on after a failed command, and a pipeline's status is that of its last command only. set -e stops the script at the first failing command, set -u makes an unset variable an error instead of an empty string, so rm -rf "$BUILD_DIR"/* cannot turn into a delete in / when the variable is missing, and pipefail makes a pipeline fail if any part fails. This script prints status without pipefail: 0, because tee succeeded, then pipeline failed: 22, then done. set -e has gaps, such as commands inside if or before ||, so check critical steps explicitly as well.

Example
#!/bin/bash
set -eu
fetch() { echo "partial"; return 22; }
fetch | tee out.txt > /dev/null
echo "status without pipefail: $?"
set -o pipefail
fetch | tee out.txt > /dev/null || echo "pipeline failed: $?"
echo "done"

Ready to check yourself? Take the free DevOps mock test

DevOps interview questions: Hard questions (experienced)

20 questions

41. How would you design CI/CD for a monorepo with 15 microservices at a Bengaluru fintech?

The goal is that a change builds and deploys only what it affects, from one shared pipeline definition. Path filters, as here, start the payments pipeline only when its own code, a shared library it uses or its workflow file changes; bigger repos use a build tool that knows the dependency graph, such as Bazel or Nx. Each service calls one reusable workflow, so scans and deploy steps live in one place. Services are versioned and deployed independently, each with its own canary and rollback. Watch the shared library: a change there must test every service that depends on it, not just one.

Example
on:
  push:
    branches: [main]
    paths:
      - 'services/payments/**'
      - 'libs/common/**'
      - '.github/workflows/payments.yml'

jobs:
  pipeline:
    uses: ./.github/workflows/service-pipeline.yml
    with:
      service: payments
    secrets: inherit

42. What is GitOps, and how does pull-based deployment differ from a push-based pipeline?

In GitOps a Git repository holds the desired state of each environment, such as which image version runs in production, and an agent inside the environment, for example Argo CD or Flux on Kubernetes, keeps pulling that repository and makes the live system match it. In push-based CD the pipeline holds credentials to the environment and runs the deploy itself. Pull-based means CI needs no production credentials, every change is a reviewed commit, rollback is a git revert, and manual changes show up as drift that can be reverted automatically. The cost is one more system to run.

43. How would you automate canary analysis so a bad release is rolled back without a human?

Decide before the release which metrics define health: error rate, p99 latency and one business metric such as successful UPI payments per minute, each compared between the canary and the stable version over the same window rather than against a fixed number. A progressive delivery tool, such as Argo Rollouts, Flagger or Spinnaker, shifts traffic in steps and queries the monitoring system at each step; if the canary is clearly worse, it sends all traffic back and marks the release failed. Give each step enough traffic and time; 1 percent of traffic for two minutes at night proves little.

44. How do you ship a database schema change without downtime when old and new versions of the app run at the same time?

Use expand and contract, spread over several releases. First expand: add the new column or table in a backward-compatible way, nullable or with a default, so the running version is unaffected. Next deploy code that writes both the old and new columns, backfill old rows in small batches to avoid long locks, and switch reads to the new column. Only when no running version uses the old column do you contract and drop it. Renaming a column in one step breaks whichever version does not expect the new name, and no rollback can bring back data a destructive migration removed.

Example
-- Release 1 (expand): new nullable column
ALTER TABLE employees ADD COLUMN mobile_e164 VARCHAR(16) NULL;

-- Release 2: app writes both columns; backfill in batches
UPDATE employees SET mobile_e164 = CONCAT('+91', mobile)
 WHERE mobile_e164 IS NULL AND id BETWEEN 1 AND 10000;

-- Release 3 (contract): no running code reads mobile
ALTER TABLE employees DROP COLUMN mobile;

45. What are feature flags, and what problems do they bring at scale?

A feature flag is a runtime switch that turns a code path on or off without a deploy, often per user group, city or percentage, for example enabling a new payroll screen for 10 percent of companies in Pune first. Flags decouple deploy from release: unfinished code can merge to main behind an off flag, which supports trunk-based development, and a bad feature can be switched off in seconds. The problems: every flag doubles the paths to test, old flags pile up as dead code, and the code must behave safely if the flag service is down. Give each flag an owner and an expiry date.

46. What is the difference between count and for_each in Terraform, and why can count be dangerous?

count creates numbered copies addressed by index, such as by_count[1], while for_each creates copies keyed by a map key or set value, such as by_key["payslips"]. The danger is that count ties each object to its position. Remove invoices from the list and the items shift: index 0 now means payslips, index 1 audit, so Terraform plans to replace by_count[0] and by_count[1] and destroy by_count[2], instead of one delete. With for_each, removing invoices destroys only by_key["invoices"]. Use count for identical copies or an on/off switch, for_each for things with an identity.

Example
variable "buckets" {
  default = ["invoices", "payslips", "audit"]
}

resource "aws_s3_bucket" "by_count" {
  count  = length(var.buckets)
  bucket = "blr-${var.buckets[count.index]}"
}

resource "aws_s3_bucket" "by_key" {
  for_each = toset(var.buckets)
  bucket   = "blr-${each.key}"
}

47. How do you rename or move resources in Terraform without destroying them?

Changing a resource's address, by renaming it or moving it into a module, looks to Terraform like deleting one object and creating another. A moved block, available since Terraform 1.1, records the old and new addresses, so the plan shows a move instead of destroy and create, and everyone who applies later gets the same move. A removed block, since 1.7, takes a resource out of Terraform's management without deleting the real object when destroy is false. Before these, people ran terraform state mv and state rm by hand, which nobody reviews and which is easy to get wrong on shared state.

Example
moved {
  from = aws_instance.web
  to   = module.web_cluster.aws_instance.this[0]
}

removed {
  from = aws_s3_bucket.old_logs

  lifecycle {
    destroy = false
  }
}

48. How would you structure Terraform for many teams and environments?

Split state by blast radius and ownership: separate states for network, data and each application, and per environment, so a bad apply cannot damage everything. Shared building blocks live in modules pinned to a tag, as here, so a module change reaches production only when a team bumps the ref after testing it. Pin provider versions and commit .terraform.lock.hcl. Run plan and apply from the pipeline, not laptops, with the plan on the pull request and policy checks before apply. CLI workspaces share one backend and its credentials, so many teams use separate state keys per environment.

Example
module "vpc" {
  source = "git::https://git.example.in/infra/tf-modules.git//vpc?ref=v3.2.0"

  name       = "payments-prod"
  cidr_block = "10.20.0.0/16"
  azs        = ["ap-south-1a", "ap-south-1b"]
}

49. How do you do a rolling update of 20 servers with Ansible without taking the service down?

Use serial so the play runs on one batch of hosts at a time: 25 percent of 20 servers means batches of 5, so four batches, and 15 servers keep serving while a batch updates. For each batch, pre_tasks drain the node from the load balancer, the role deploys, and post_tasks wait for the health check before putting it back. Without max_fail_percentage, Ansible moves on to the next batch unless every host in the current one failed; with 0, a single failure stops the rollout, so a bad build reaches 5 servers, not 20. A list such as serial: [1, "25%"] adds a one-server canary batch first.

Example
- hosts: app_servers
  become: true
  serial: "25%"
  max_fail_percentage: 0
  pre_tasks:
    - name: Drain node from the load balancer
      ansible.builtin.uri:
        url: "http://{{ lb_host }}/api/drain/{{ inventory_hostname }}"
        method: POST
      delegate_to: localhost
  roles:
    - app_deploy
  post_tasks:
    - name: Wait until the app is healthy
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}:8080/health"
      register: health
      retries: 10
      delay: 6
      until: health.status == 200
    - name: Put node back in the load balancer
      ansible.builtin.uri:
        url: "http://{{ lb_host }}/api/enable/{{ inventory_hostname }}"
        method: POST
      delegate_to: localhost

50. How do you keep Jenkins pipelines consistent across 50 repositories, and how do you scale Jenkins itself?

Move the common logic into a Jenkins shared library: a separate Git repository with custom steps in its vars folder, so each Jenkinsfile shrinks to a call like this one, pinned to a library version. A fix or a new security scan then lands in one place, and teams adopt it by bumping the version. To scale, run builds only on ephemeral agents, such as cloud VMs or Kubernetes pods created per build, keep zero executors on the controller, and manage the controller's own setup with Configuration as Code and a short, reviewed plugin list. Plugin sprawl is the usual cause of painful Jenkins upgrades.

Example
@Library('company-pipelines@v2.3.0') _

standardServicePipeline(
    service: 'leave-api',
    javaVersion: '21',
    deployTo: ['staging', 'prod']
)

51. How do needs and parent-child pipelines help with a large GitLab CI pipeline?

By default a stage waits for every job in the previous stage, so a slow web build delays the API tests. needs turns the pipeline into a graph: test-api starts as soon as build-api finishes, whatever else is still running, and it downloads artifacts only from the jobs it needs. Parent-child pipelines split a huge file: the mobile job triggers a separate child pipeline from its own YAML, which can be generated or run only when mobile code changes, and strategy: depend makes the trigger job wait for the child and take its status. Together they cut waiting time and keep configs readable.

Example
stages: [build, test]

build-api:
  stage: build
  script: make api

test-api:
  stage: test
  needs: [build-api]
  script: make test-api

build-web:
  stage: build
  script: make web

test-web:
  stage: test
  needs: [build-web]
  script: make test-web

mobile:
  stage: build
  trigger:
    include: mobile/.gitlab-ci.yml
    strategy: depend

52. How do you enforce a standard, secure pipeline across many teams in Azure DevOps?

Put the approved pipeline in a central template repository and have every team extend it. With extends, the template owns the whole structure and the team passes only parameters, so it cannot drop the security scan or skip the approval; the template can also limit which steps a parameter may inject. Then enforce it with a Required template check on the production environment or service connection, so a pipeline that does not extend the approved template is stopped before it can deploy. Pin the template repository to a tag, as here, so template changes roll out deliberately.

Example
resources:
  repositories:
    - repository: templates
      type: git
      name: Platform/pipeline-templates
      ref: refs/tags/v1.4.0

extends:
  template: secure-service.yml@templates
  parameters:
    serviceName: attendance-api
    runIntegrationTests: true

53. How would you add security checks to CI/CD without slowing every team down?

Shift left, in layers. On every pull request run the fast checks: secret scanning, dependency scanning for known vulnerabilities, static analysis of the changed code, and scans of infrastructure code such as Terraform files. Scan each built container image, and run dynamic scans against staging nightly, because they are slow. Fail the build only on new high or critical findings that have a fix, and send the rest to a backlog with an owner; a gate that fails on hundreds of old warnings gets switched off within a week. Show results inside the pull request and track time to fix, not just counts.

54. Which GitHub Actions settings reduce the risk of a supply-chain attack through your workflows?

Give GITHUB_TOKEN the least permission it needs, set with permissions, so a compromised step cannot push code or create releases. Pin third-party actions to a full commit SHA, because a tag such as v2 can be moved to malicious code by whoever controls that repository; Dependabot can still update the pins. For code from forks use the pull_request trigger, which runs without your secrets. Be careful with pull_request_target: it runs with secrets and a write token, so never check out and run the fork's code in it. persist-credentials: false stops checkout leaving the token in the git config.

Example
on: pull_request

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      # third-party action pinned to a full commit SHA
      - uses: some-org/setup-tool@<full-40-char-commit-sha>
      - run: npm ci
      - run: npm test

55. A developer pushed an AWS access key to a public GitHub repository. What do you do, in order?

First deactivate the key in AWS at once, before anything else, because bots scan public repositories within minutes, and deleting the commit does not help: the key survives in forks, caches and history. Then check CloudTrail for what the key did since the push, such as new instances for crypto mining or S3 reads, and contain any damage. Replace the credential through the proper path, ideally a role or OIDC so no long-lived key is needed at all. Then clean the history if you wish, turn on secret scanning with push protection, and write a blameless postmortem on why a key existed on a laptop.

56. What is burn-rate alerting on an SLO, and why is it better than a fixed error threshold?

Burn rate is how fast you spend the error budget compared with a steady pace: at a burn rate of 1 the budget runs out exactly at the end of the 30-day window. A widely used setup pages when the burn rate is above 14.4 over the last hour, which means 2 percent of the monthly budget went in that hour, and above 6 over six hours, 5 percent; a burn rate of 1 over three days, 10 percent, raises a ticket. Pairing each long window with a short one stops the alert firing after recovery. A fixed error threshold pages for harmless blips or misses slow burns.

Example
SLO 99.9% over 30 days (720 h), budget = 0.1%
burn rate = observed error rate / 0.1%

burn 14.4 for 1 h  -> 14.4 x 1 / 720  = 2% of budget
                     (error rate 1.44%, budget gone in 50 h)
burn 6 for 6 h     -> 6 x 6 / 720     = 5% of budget
burn 1 for 72 h    -> 1 x 72 / 720    = 10% of budget

57. Your API depends on three services in series with 99.9, 99.95 and 99.99 percent availability. What can the API promise, and how do you improve it?

When every dependency must work, availabilities multiply: 0.999 x 0.9995 x 0.9999 is about 0.9984, roughly 99.84 percent, or about 69 minutes of downtime in 30 days, before counting the API's own failures. The result is always below the weakest single dependency. To promise more, remove hard dependencies from the request path, add redundancy, since two independent 99.9 percent replicas give 99.9999 percent if their failures really are independent, and degrade gracefully with caches, timeouts and fallbacks. Shared causes, such as one region or one database, break that independence.

Example
serial:    A = 0.999 x 0.9995 x 0.9999 = 0.99840 (about 99.84%)
downtime:  (1 - 0.99840) x 43,200 min = about 69 min per 30 days
redundant: A = 1 - (1 - 0.999) x (1 - 0.999) = 0.999999

58. Why is high label cardinality a problem in Prometheus, and how do you avoid it?

Prometheus stores one time series for every unique combination of label values, and memory and query cost grow with the number of series. Bounded labels are fine: 40 routes, 5 methods and 8 status codes give at most 1,600 series per instance. Add a user_id label for 2 lakh users and the worst case becomes 320 million series, which can exhaust memory, slow every query and crash the server. Keep labels to small, known sets, use the route template instead of the raw path, and put per-user or per-order detail in logs or traces. Watch the series count per metric to catch a bad label early.

Example
# risky: one time series per order and per user
http_requests_total{path="/orders/98213", user_id="u_55120"}

# better: small, bounded label sets
http_requests_total{route="/orders/:id", method="GET", code="200"}

59. What are the common mistakes when an organisation adopts DORA metrics?

The biggest is turning them into targets for individuals or for ranking teams: once a bonus depends on deployment frequency, people split one change into ten deploys and the number stops meaning anything. The metrics describe a team's delivery system and are meant for that team to find its own bottlenecks. Other mistakes are reading one metric alone, since faster deploys with a rising failure rate is not progress, collecting the data by hand, defining failure differently in each team, and comparing a mobile app team with a web service team. Follow the trend over months.

60. How do you run a major production incident and the postmortem afterwards?

During the incident, assign roles: an incident commander who coordinates and decides, engineers who investigate and fix, and one person who updates stakeholders on a fixed schedule so the engineers are not interrupted. Restore service first, by rolling back or switching off a feature flag, and find the root cause later. Keep a timeline in the incident channel. Afterwards write a blameless postmortem within a few days: what happened, the impact, how it was detected, why the system allowed it, and action items with owners and dates. Blame makes people hide mistakes; fixing the process lasts.

Practice questions for November 2026

New every month

3 questions from the DevOps mock test, the way they appear in it. A new set comes on the 1st of every month. Pick your answer, then open it.

  1. FresherJenkins

    Jenkins checks the Git server every minute for new commits (Poll SCM). What is usually a better way to start a build on each push?

    • Asking every developer to press Build Now in Jenkins after each push
    • Polling every five seconds instead of every minute, so builds start sooner
    • A nightly cron trigger that builds whatever was pushed during the day
    • A webhook from the Git server that tells Jenkins as soon as a push happens
    Show the answer

    Answer: A webhook from the Git server that tells Jenkins as soon as a push happens. Polling makes Jenkins ask the Git server again and again, even when nothing changed, which adds load and delay. A webhook sends an event the moment a commit lands, so builds start at once with no wasted requests. A nightly build gives much slower feedback.

  2. Intermediate (1-5 years)Monitoring and Alerting

    Ten requests took 120, 95, 110, 2400, 105, 130, 98, 101, 115 and 125 ms. Using the nearest-rank method, what are the mean and the p90?

    • Mean 339.9 ms, p90 2400 ms
    • Mean 112.5 ms, p90 130 ms
    • Mean 130 ms, p90 339.9 ms
    • Mean 339.9 ms, p90 130 ms
    Show the answer

    Answer: Mean 339.9 ms, p90 130 ms. Sorted, the 9th of 10 values (rank = ceil(0.9 x 10)) is 130 ms, so 90% of requests took 130 ms or less, while one 2,400 ms outlier drags the mean up to 339.9 ms. Neither number alone tells the full story; p99 and max show the outlier.

  3. Experienced (5+ years)Terraform

    Autoscaling raises desired_capacity to 6 during a peak. What does the next terraform plan show for that attribute?

    resource "aws_autoscaling_group" "web" {
      name             = "web"
      min_size         = 2
      max_size         = 10
      desired_capacity = 2
      # launch template and subnets omitted
      lifecycle {
        ignore_changes = [desired_capacity]
      }
    }
    • A change back to 2, which would shrink the group in the middle of the peak
    • No change, since ignore_changes leaves desired_capacity as it is
    • An error, because a value that changed outside Terraform cannot be ignored
    • A replacement of the group, because desired_capacity forces a new resource
    Show the answer

    Answer: No change, since ignore_changes leaves desired_capacity as it is. Without ignore_changes, Terraform sees drift and plans to reset desired_capacity to 2, fighting the autoscaler. ignore_changes keeps the value from creation but skips later differences, which suits attributes owned by another system.

Take the full DevOps mock test

Ready to apply?

There are 2,051 open jobs asking for DevOps on CoreHRx right now. Create a free profile once and apply with it.