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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- azure-bicep Installs to ~/.agents/skills/azure-bicep/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install azure-bicep The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project azure-bicep Installs to .agents/skills/azure-bicep/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/azure-bicep.zipunzip -o azure-bicep.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) azure-bicep PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- The team standardises on Terraform for Azure: use terraform-modules
- You only need to deploy app code to an existing web app: use azure-app-service
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.
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
- Read
AGENTS.md, the repo’sinfra/layout,bicepconfig.json, and the CI workflow that deploys it. Follow the existing scope (resource group vs subscription) and naming. - 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.xorres.id) overdependsOn. - Use symbolic references (
storage.id,storage.properties.primaryEndpoints.blob), notresourceId()string building, for resources in the same file. - Recent Bicep versions make the module
nameoptional; 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-versionslinter 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 Completedeletes 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.deleteAllas 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
nameorlocationcreates a new resource; the old one stays (Incremental) or is deleted (Complete). existingresources in another resource group needscope: 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.yaml
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.