DEV Community

Cover image for Review Bicep before deployment: what-if, linter and cost
Prateek Singh for Cloudeval AI

Posted on Originally published at cloudeval.ai

Review Bicep before deployment: what-if, linter and cost

TL;DR: To review a Bicep change before deployment, compile it with az bicep build, run the Bicep linter, az deployment group validate and what-if, then review the compiled ARM JSON for design, cost and security. what-if shows what will change in Azure. It doesn't tell you whether the change is safe, affordable or exposed.

By Prateek Singh, founder of Ganak AI Labs. Updated 6 October 2026.

Your pull request touches main.bicep. CI is green, and what-if prints a tidy list of + Create and ~ Modify lines. Someone approves it.

Two weeks later you learn the "small" change opened a storage account to the internet, or doubled the bill with a new SKU.

None of the tools lied. what-if tells you what will change, not whether it's safe. Here's how to answer that second question in the same PR, plus a checklist to copy.

Disclosure: I'm the founder of Cloudeval.

Key terms used below

  • Bicep: Microsoft's language for Azure infrastructure. It compiles to an ARM template.
  • ARM template: the Azure Resource Manager JSON file that Azure actually deploys.
  • what-if: an Azure CLI command that previews changes against what's already deployed.
  • Bicep linter: the built-in checker, configured in bicepconfig.json.

What does each Bicep pre-deployment check catch?

Check Catches Needs Azure access? Misses
az bicep build Syntax and type errors No Intent and design
Bicep linter Hard-coded URLs, secrets in outputs, insecure defaults, old API versions No Topology, cost, cross-resource risk
az deployment group validate Whether Resource Manager accepts the template at a real scope Yes Whether you should deploy it
what-if The resource-level diff against current Azure state Yes, deploy-level Whether that diff is safe or affordable
Design, cost and security review Public exposure, dependency shape, cost drivers Not for template review Runtime behaviour, live drift

Keep the first four. The gap is the last row.

How do I review a Bicep change before deployment?

  1. Compile the Bicep file to ARM JSON with az bicep build.
  2. Run the native checks: the linter, validate and what-if.
  3. Review design, cost and security on the compiled ARM JSON, locally or on every pull request.
  4. Read the findings in a fixed order, then re-run what-if right before you deploy.

Step 1: Compile with az bicep build

Bicep compiles to ARM JSON, and that's what Azure actually deploys.

az bicep build --file ./infra/main.bicep --outfile ./infra/main.json
Enter fullscreen mode Exit fullscreen mode

Compile a .bicepparam file the same way:

az bicep build-params --file ./infra/main.bicepparam --outfile ./infra/main.parameters.json
Enter fullscreen mode Exit fullscreen mode

Make this your first CI step. Skim the JSON on module changes too. A one-line version bump can change many resources, and only the compiled output shows it.

Step 2: Run the native checks (linter, validate, what-if)

The linter

Set the rules that matter for review to error in bicepconfig.json:

{
  "analyzers": {
    "core": {
      "enabled": true,
      "rules": {
        "outputs-should-not-contain-secrets": { "level": "error" },
        "secure-parameter-default": { "level": "error" },
        "use-secure-value-for-secure-inputs": { "level": "error" },
        "no-hardcoded-environment-urls": { "level": "error" },
        "use-recent-api-versions": { "level": "warning" }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Then run it in CI:

az bicep lint --file ./infra/main.bicep
Enter fullscreen mode Exit fullscreen mode

validate and what-if

These two need a real Azure scope:

az deployment group validate \
  --resource-group rg-app-dev \
  --template-file ./infra/main.bicep \
  --parameters ./infra/main.bicepparam

az deployment group what-if \
  --resource-group rg-app-dev \
  --template-file ./infra/main.bicep \
  --parameters ./infra/main.bicepparam
Enter fullscreen mode Exit fullscreen mode

sub, mg and tenant variants cover other scopes. Keep in mind:

  • what-if needs deploy permissions, including Microsoft.Resources/deployments/*. Reviewers often lack them.
  • Azure-set defaults can show up as changes that won't happen.
  • Resources deployed through templateLink, including template specs, aren't expanded.

For refactors, bicep snapshot (Bicep CLI 0.41.2+) diffs what a .bicepparam would deploy, without contacting Azure:

bicep snapshot --mode overwrite ./infra/main.bicepparam   # on main
bicep snapshot --mode validate  ./infra/main.bicepparam   # on the PR branch
Enter fullscreen mode Exit fullscreen mode

You now know what will change. You still don't know if it's a good idea.

Step 3: Review design, cost and security

Most pipelines skip this layer because it meant reading raw JSON by hand. Use any tool your team trusts. PSRule for Azure is a solid open-source option.

I'll use Cloudeval here. It reviews the compiled ARM JSON and returns architecture and dependency diagrams with cost and security findings. It doesn't read .bicep files directly, which is why Step 1 matters.

Here's a two-minute demo:

Option A: from your terminal

Install and sign in:

npm install -g @ganakailabs/cloudeval-cli   # Node.js 20+
cloudeval login            # or: cloudeval login --headless  (SSH/containers)
cloudeval auth status
Enter fullscreen mode Exit fullscreen mode

Create a project from the compiled template and run every report:

cloudeval projects create \
  --template-file ./infra/main.json \
  --parameters-file ./infra/main.parameters.json \
  --name "PR 1234 - main.bicep" \
  --provider azure \
  --format json \
  --output ./cloudeval-project.json

PROJECT_ID=$(jq -r '.data.project.id' ./cloudeval-project.json)

cloudeval reports run --project "$PROJECT_ID" --type all --wait --format json
Enter fullscreen mode Exit fullscreen mode

This runs against the template, not your subscription, so you need a Cloudeval account but no Azure credentials.

Read the results:

cloudeval reports waf  --project "$PROJECT_ID" --format markdown   # architecture findings
cloudeval reports cost --project "$PROJECT_ID" --format markdown   # cost estimate

cloudeval issues list \
  --project "$PROJECT_ID" \
  --type architecture,cost \
  --severity critical,high \
  --format json
Enter fullscreen mode Exit fullscreen mode

Cloudeval Top Actions list with findings such as Azure.Storage.BlobPublicAccess and Azure.Storage.Firewall, each tagged Security, Cost or Operational
Each finding names the rule, the resource type and the pillar.

Or open the diagram:

cloudeval open project "$PROJECT_ID" \
  --view preview --layout dependency --print-url --no-open
Enter fullscreen mode Exit fullscreen mode

Cloudeval split view with the generated architecture diagram on the left and the project file explorer and ARM JSON outline on the right
The generated diagram sits next to your files and ARM JSON outline.

Prefer clicking? In the web app, choose Projects, then Create project, then Upload Template File, and drop in main.json (up to 2 MB). The import guide covers URLs, parameter files and linked templates.

Imported ARM template beside project files and the generated ARM JSON outline in Cloudeval
Your template, ARM JSON outline and report history live in one workspace.

Option B: on every pull request (GitHub Action)

ganakailabs/cloudeval-action posts one result comment per PR run. It can also write SARIF output and enforce merge gates.

Setup takes three steps (see the GitHub Actions guide):

  1. Link the repo to a Cloudeval project with the Cloudeval GitHub App.
  2. Create a CI access key scoped to that project.
  3. Add the CLOUDEVAL_ACCESS_KEY and CLOUDEVAL_PROJECT_ID repository secrets.

One Bicep detail: the action reviews the synced files at the pushed commit, and it reads ARM JSON. So commit the compiled infra/main.json and main.parameters.json, and fail CI if they're stale.

name: Bicep review
on:
  pull_request:

permissions:
  contents: read
  pull-requests: write
  issues: write

jobs:
  native-checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: az bicep lint --file ./infra/main.bicep
      - name: Compiled ARM JSON must be up to date
        run: |
          az bicep build --file ./infra/main.bicep --outfile ./infra/main.json
          git diff --exit-code -- ./infra/main.json

  cloudeval-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ganakailabs/cloudeval-action@v1
        with:
          access_key: ${{ secrets.CLOUDEVAL_ACCESS_KEY }}
          project_id: ${{ secrets.CLOUDEVAL_PROJECT_ID }}
          mode: review
          post_pr_comment: true
          upload_artifacts: true
Enter fullscreen mode Exit fullscreen mode

Keep the jobs separate. The review job stops on a dirty working tree, and building inside it would dirty it.

Set the entry point and gates in .cloudeval/config.yaml at the root of the synced source. With a source root of infra, that's infra/.cloudeval/config.yaml:

version: 1
stacks:
  - id: main
    entry: main.json                   # relative to the source root: infra/main.json -> main.json
    parameters: main.parameters.json
resolve:
  linked_templates: true
ci:
  gates:
    enforcement: comment_only   # switch to block_pull_request once thresholds are tuned
    minimum_well_architected_score: 80   # the default once ci.gates exists; set your own floor
    fail_when_high_risk_findings_exist: true
    fail_when_validation_fails: true
    max_monthly_cost_usd: 500
Enter fullscreen mode Exit fullscreen mode

Keys you leave out of ci.gates take defaults, so set the score floor on purpose. Start with comment_only. Once you trust the thresholds, switch to block_pull_request and make the job a required status check.

Step 4: Read the findings

The PR comment keeps your gate separate from what the review saw:

Overall: PASS
Well-Architected posture: 23.1/100 (critical)
Validation: 3 unit tests failed
Policy checks: good
Cost: 143.81 USD/mo (under 100K budget)
Enter fullscreen mode Exit fullscreen mode

Overall reflects your thresholds. critical is the posture Cloudeval observed. A PR can pass a loose gate and still deserve a hard look.

Expanded Cloudeval PR comment showing validation details (unit tests and policy checks) and architecture signals such as resource count, relationships and high-risk findings
The expanded comment shows validation results and architecture signals.

Read the findings in this order:

  1. Dependency view. Does the topology match the PR description? Look for surprise public endpoints, identity or subnet changes.
  2. High-severity findings. Did this PR change the affected resource?
  3. Cost drivers. Should a SKU change before rollout? Numbers are estimates.
  4. Gaps. "Not assessed" doesn't mean safe.

Cloudeval dependency view of an Azure template with one resource selected and its upstream and downstream paths highlighted
Select a resource to see its upstream and downstream paths.

Then run what-if at deploy time anyway.

What belongs in a Bicep pre-deploy checklist?

## Bicep pre-deploy review
- [ ] `az bicep build` passes, compiled ARM JSON is committed and up to date
- [ ] `az bicep lint` clean (security-relevant rules set to `error`)
- [ ] `az deployment group validate` passes at the target scope
- [ ] `what-if` diff matches the PR description (no surprise Delete/Modify)
- [ ] Linked/nested templates and template specs reviewed separately (what-if doesn't expand templateLink)
- [ ] Dependency diagram reviewed: no unintended public exposure, identity or subnet changes
- [ ] High/critical findings triaged: fixed, waived with reason, or ticketed
- [ ] Monthly cost estimate checked against budget, SKU choices justified
- [ ] "Not assessed" areas noted, not assumed safe
- [ ] what-if re-run immediately before the real deployment
Enter fullscreen mode Exit fullscreen mode

What doesn't a template review catch?

Cloudeval adds diagrams, cost and security findings from 1,650+ checks, right in the PR. It covers Azure, plus AWS CloudFormation (beta).

It can't prove runtime behaviour, and it doesn't replace what-if, the only real answer to "what changes in this subscription right now". It doesn't catch live drift yet either. Drift comparison is planned, and live Azure state comes in through a separate Cloud sync.

Frequently asked questions

How do I preview Azure deployment changes before deploying?

Run az deployment group what-if with the template and parameters you'll deploy. It lists each resource that would be created, modified or deleted, and needs deploy permissions.

Why does Bicep what-if show changes that won't happen?

Properties Azure sets as defaults can show up as changes even though nothing will change. Check unexpected Delete or Modify lines against the PR description first.

Can I review a Bicep change without access to the Azure subscription?

Partly. build, the linter and bicep snapshot run locally, but validate and what-if need deploy-level access. Cloudeval works from the compiled ARM JSON, so you need a Cloudeval account, not subscription credentials.

Does Cloudeval read .bicep files directly?

No. Compile with az bicep build and review the ARM JSON. That's what Azure actually deploys anyway.

How do I block a merge on high-risk findings or cost?

Add ci.gates to .cloudeval/config.yaml, for example fail_when_high_risk_findings_exist or max_monthly_cost_usd. Set enforcement: block_pull_request and make the job a required check in branch protection.


For the longer version, read How do I review a Bicep or ARM template before deployment? Or browse a public example PR with real caveats, including failed validation tests.

Related on cloudeval.ai

Try it

Compile your Bicep, then review the ARM JSON before it ships. Open the Cloudeval app and follow the IaC pre-deployment review guide.

About the author

Prateek Singh is the founder of Ganak AI Labs and builds Cloudeval AI.

cloudeval.ai | Docs | YouTube | GitHub | LinkedIn

Top comments (0)