Azure Bicep skill for Codex

Typed templates, a what-if before every deploy. Codex writes Bicep with typed, validated parameters, per-environment .bicepparam files and symbolic references instead of string-built resource IDs. It checks Azure Verified Modules before hand-rolling one, keeps secrets out of outputs, uses deterministic guid() names for role assignments, and shows a what-if before any deployment runs.

  • Cloud Platforms
  • $azure-bicep
  • 264 lines
  • 2 files
  • ~2,100 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- azure-bicep

Installs to ~/.agents/skills/azure-bicep/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $azure-bicep, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and reviews Azure Bicep infrastructure as code - main.bicep with typed parameters and .bicepparam files, reusable modules and Azure Verified Modules from the public registry, user-defined types, existing resources, loops and conditions, secure parameters, bicepconfig.json linting, what-if previews and deployment stacks. Use when creating or refactoring Bicep files, converting ARM JSON or portal resources to Bicep, splitting templates into modules, reviewing a what-if before an Azure deployment, or debugging Bicep build and deployment errors.

Use it when

  • Creating or refactoring Bicep templates and modules
  • Converting ARM JSON or portal-built resources to Bicep
  • Reviewing an Azure deployment before it runs
  • Bicep build, linter or deployment errors

Use something else when

How to check the result

Bicep build and lint pass with no warnings, the what-if shows only intended creates and modifies, and a second what-if after deploying shows no changes.

The mistake it prevents

Outputting a listKeys() result, which stores the secret in deployment history readable by anyone with Reader on the resource group.

~/.agents/skills/azure-bicep/SKILL.md

--- name: azure-bicep description: Writes and reviews Azure Bicep infrastructure as code - main.bicep with typed parameters and .bicepparam files, reusable modules and Azure Verified… license: MIT ---

Done well means: templates build with zero linter warnings, parameters are typed and validated, environments differ only by .bicepparam files, secrets never appear in outputs or plain parameters, and every deployment is preceded by a what-if the user has read.

Inspect first

  1. Read AGENTS.md, the repo’s infra/ layout, bicepconfig.json, and the CI workflow that deploys it. Follow the existing scope (resource group vs subscription) and naming.
  2. Check tool versions; Bicep moves fast:
az bicep version || az bicep install      # install needs network
bicep --version                           # standalone CLI, if used instead
az bicep build --file infra/main.bicep --stdout > /dev/null   # compile + lint

Layout

infra/
├── main.bicep              # composes modules; scope declared at the top
├── main.dev.bicepparam
├── main.prod.bicepparam
├── modules/
│   ├── web.bicep
│   └── data.bicep
├── types.bicep             # exported user-defined types
└── bicepconfig.json

Template conventions

targetScope = 'resourceGroup'

import { appSettingsType } from 'types.bicep'

@description('Short environment name used in resource names.')
@allowed(['dev', 'test', 'prod'])
param env string

param location string = resourceGroup().location

@minValue(1)
@maxValue(10)
param webInstances int = env == 'prod' ? 2 : 1

param appSettings appSettingsType = {}

var suffix = uniqueString(resourceGroup().id)
var tags = { env: env, owner: 'shop-team', managedBy: 'bicep' }

resource kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
  name: 'shop-kv-${env}'
}

module web 'modules/web.bicep' = {
  params: {
    name: 'shop-web-${env}-${suffix}'
    location: location
    instances: webInstances
    tags: tags
    appSettings: appSettings
    dbPassword: kv.getSecret('db-password')   // only valid for @secure() module params
  }
}

output webHostName string = web.outputs.hostName
  • Prefer implicit dependencies (referencing web.outputs.x or res.id) over dependsOn.
  • Use symbolic references (storage.id, storage.properties.primaryEndpoints.blob), not resourceId() string building, for resources in the same file.
  • Recent Bicep versions make the module name optional; keep an explicit name when two module deployments could collide in the same scope (loops, parallel pipelines).
  • Pin API versions explicitly; let the use-recent-api-versions linter rule flag stale ones rather than bumping blindly, since property shapes change between versions.

Types, loops, conditions

// types.bicep
@export()
type appSettingsType = {
  @description('Key/value pairs passed to the web app')
  *: string
}

@export()
type subnetType = {
  name: string
  prefix: string
  delegation: string?
}
param subnets subnetType[]
param deployRedis bool = false

resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' = {
  name: 'shop-vnet'
  location: location
  properties: {
    addressSpace: { addressPrefixes: ['10.20.0.0/16'] }
    subnets: [for s in subnets: {
      name: s.name
      properties: {
        addressPrefix: s.prefix
        delegations: s.delegation == null ? [] : [{ name: 'd', properties: { serviceName: s.delegation! } }]
      }
    }]
  }
}

module redis 'modules/redis.bicep' = if (deployRedis) {
  params: { location: location }
}

Parameters files

// main.prod.bicepparam
using './main.bicep'

param env = 'prod'
param webInstances = 3
param appSettings = {
  FEATURE_X: 'on'
}

Secrets do not belong in .bicepparam files. Read them at deploy time with getSecret() on an existing Key Vault, or in the params file with az.getSecret('<subscription-id>', '<rg>', '<vault>', '<secret>').

Registry modules (Azure Verified Modules)

Before writing a module for a common resource, check for an AVM module:

module storage 'br/public:avm/res/storage/storage-account:<version>' = {
  params: {
    name: 'shopst${suffix}'
    skuName: 'Standard_ZRS'
    allowBlobPublicAccess: false
  }
}

Pin an exact version from the module’s published list (needs network to restore). Read its README for parameter names instead of guessing; AVM modules are strict about types.

Security rules

  • @secure() on every password, key or connection string parameter. Never default them.
  • Never output secrets: listKeys() in an output lands in deployment history readable by anyone with Reader. Pass resource IDs and let consumers use managed identity.
  • Prefer managed identity + role assignments in Bicep over keys:
resource blobReader 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(storage.id, web.outputs.principalId, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
  scope: storage
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') // Storage Blob Data Contributor
    principalId: web.outputs.principalId
    principalType: 'ServicePrincipal'
  }
}

The role assignment name must be a deterministic guid() or redeploys fail with “role assignment already exists”.

bicepconfig.json

{
  "analyzers": {
    "core": {
      "enabled": true,
      "rules": {
        "no-hardcoded-env-urls": { "level": "error" },
        "secure-parameter-default": { "level": "error" },
        "outputs-should-not-contain-secrets": { "level": "error" },
        "use-recent-api-versions": { "level": "warning" },
        "no-unused-params": { "level": "error" }
      }
    }
  }
}

Deploy workflow

- [ ] az bicep build (or bicep lint) passes with no warnings
- [ ] az deployment group what-if reviewed with the user
- [ ] No unexpected Delete or Modify on stateful resources (databases, vaults, storage)
- [ ] User confirms, then deploy
- [ ] Outputs checked; smoke test the deployed endpoints
az deployment group what-if -g "$RG" --template-file infra/main.bicep \
  --parameters infra/main.prod.bicepparam
az deployment group create -g "$RG" --name "shop-$(date +%Y%m%d%H%M)" \
  --template-file infra/main.bicep --parameters infra/main.prod.bicepparam \
  --confirm-with-what-if
  • Default mode is Incremental: resources missing from the template are left alone. --mode Complete deletes them; never use it without an explicit user decision.
  • Deployment stacks (az stack group create ... --action-on-unmanage detachAll --deny-settings-mode denyDelete) track managed resources and can block out-of-band deletes. deleteAll as the unmanage action removes resources dropped from the template; confirm before using it.
  • What-if has noise (properties the RP normalises). Read it for Create, Delete and changes to SKU, network rules, identity and keys.

Converting existing resources

az bicep decompile --file exported.json                       # ARM JSON -> Bicep, then clean up
az resource show --ids <resourceId> --query properties        # check real settings

Decompiled output is a starting point: rename symbols, remove read-only properties, parameterise names, and run what-if against the live resource until it shows no changes.

Gotchas

  • Changing a resource name or location creates a new resource; the old one stays (Incremental) or is deleted (Complete).
  • existing resources in another resource group need scope: resourceGroup('other-rg').
  • Loops over modules deploy in parallel; add @batchSize(1) when order matters.
  • Child resources declared both nested and top-level conflict; pick one style.
  • Subscription-scope templates need az deployment sub create --location.

Verification

  • Build and lint are clean; what-if shows only intended changes.
  • Re-running the same deployment produces a what-if with no changes (idempotent).
  • Deployed resources match tags, SKUs and network settings in the params file.

Report template

**Scope:** resource group rg-shop-prod, main.bicep + main.prod.bicepparam
**Changes (what-if):** +2 create (redis, role assignment), ~1 modify (web app settings), 0 delete
**Modules:** web, data (local); storage-account from AVM (pinned version)
**Security:** 2 @secure params via Key Vault, no secret outputs, linter clean
**Deployed:** shop-202611041530 succeeded; second what-if shows no changes

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Azure Bicep"
  short_description: "Typed templates, a what-if before every deploy"
  default_prompt: "$azure-bicep Split this 900-line main.bicep into modules with dev and prod bicepparam files"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc