Jon Gallant
azd exec Is Now an Official azd Command

azd exec Is Now an Official azd Command

6 min read

I’ve been annoyed by this problem for years. I’d be working with a customer on an Azure project, they’d need to run a database migration, and we’d spend 20 minutes just getting the environment variables sorted. You know the drill - azd env get-values > .env, then source .env (or the PowerShell equivalent on Windows), hope you didn’t forget to re-export after changing something. Or write a wrapper script that does all that for you. Every. Single. Time.

I spent way too many hours watching people fumble through this. So I built azd exec. Here’s the story of how it went from a side project to an official azd command.

The backstory

This wasn’t a new ask. People had been filing issues about this since 2022 - #391, #1697, #2336, #4067. All variations of the same thing: “I just want to run a command with my azd environment loaded.”

I built it as an independent extension first. My original plan was to keep it as an extension - that let me iterate fast, test the design, and ship something people could use right away without waiting for it to land in core. The extension approach also meant I could validate the UX and API surface before committing to anything permanent.

When I submitted the PR to bring it into the azure-dev repo, I proposed it as an installable extension there too. But the core team pushed back - in a good way. They felt this was fundamental enough that it shouldn’t require an extra install step. It should just be part of azd. They convinced me, and honestly they were right. Running scripts with your environment context isn’t a nice-to-have, it’s something you do every day.

So it went in as a core part of azd. 2,022 lines added, 23 tests at 94.3% coverage, merged on May 12th.

What it does

Here’s the simplest version:

Terminal window
azd exec python script.py

That runs python script.py with every azd environment variable injected into the process. Your script gets AZURE_ENV_NAME, AZURE_SUBSCRIPTION_ID, AZURE_LOCATION, AZURE_RESOURCE_GROUP, and every custom variable you’ve set with azd env set.

No sourcing files. No exporting. No wrapper scripts. One command.

How it decides what to do

azd exec has three execution modes, and it picks the right one automatically:

What you typeWhat happens
azd exec python script.pyDirect process execution - runs python with script.py as an argument, no shell involved
azd exec './deploy.sh'Script file execution - detects shell from the file extension and runs it
azd exec 'echo $AZURE_ENV_NAME'Shell inline - wraps it in bash -c (or your specified shell)

The heuristic is straightforward: multiple arguments without --shell means direct exec. A single quoted argument means shell inline. A file path means script execution. You can always override with --shell:

Terminal window
azd exec --shell pwsh 'Write-Host $env:AZURE_STORAGE_ACCOUNT'

Real scenarios where this matters

Database migrations

This is the one that got me to build it. You have a migration script that needs a connection string. That connection string lives in your azd environment, maybe with a Key Vault reference for the password:

Terminal window
# Before: manual env setup, hope you got everything
azd env get-values > .env
source .env
python manage.py migrate
# After: one command
azd exec python manage.py migrate

Your migration script just reads DATABASE_URL from the environment. azd exec already put it there, including resolving any Key Vault secrets.

Running your app locally

Terminal window
azd exec npm run dev

Your local dev server starts with the full Azure context. Every process.env.AZURE_* variable is populated. If you’re using Key Vault references for secrets like API keys or connection strings, those get resolved to actual values before your app sees them.

Deploy scripts

Terminal window
azd exec ./scripts/deploy.sh --environment staging

The -- separator isn’t even needed here since --environment is clearly a script argument. But if your flags collide with azd exec’s own flags, you can use it:

Terminal window
azd exec -- python app.py --interactive

CI/CD pipelines

Exit codes propagate faithfully. If your script exits with code 1, azd exec exits with code 1. This makes it safe to use in GitHub Actions or Azure Pipelines:

- name: Run database migration
run: azd exec python manage.py migrate
- name: Run integration tests
run: azd exec pytest tests/integration/

Seed scripts with Key Vault secrets

#!/bin/bash
# seed.sh - seeds the database with test data
# DATABASE_PASSWORD is auto-resolved from Key Vault
echo "Seeding $AZURE_ENV_NAME environment..."
psql "host=$DB_HOST dbname=$DB_NAME user=$DB_USER password=$DATABASE_PASSWORD" \
-f ./seed-data.sql
Terminal window
azd exec ./seed.sh

The Key Vault integration is the part I’m most happy with. If you’ve set an environment variable like:

Terminal window
azd env set DATABASE_PASSWORD @Microsoft.KeyVault(VaultName=myvault;SecretName=db-password)

Then azd exec resolves that to the actual secret value before your script runs. Your script just sees the password in $DATABASE_PASSWORD. No Key Vault SDK calls, no managed identity boilerplate in your script.

Cross-platform shell support

This was important. azd exec supports bash, sh, zsh, pwsh, powershell, and cmd. It auto-detects the right shell from file extensions:

  • .sh -> bash (or sh)
  • .ps1 -> pwsh
  • .cmd, .bat -> cmd

Or you can be explicit:

Terminal window
azd exec --shell pwsh ./setup.ps1
azd exec --shell zsh ./deploy.zsh

Interactive mode

Some scripts need user input - prompts, confirmations, interactive wizards. Use -i:

Terminal window
azd exec -i ./interactive-setup.sh

This passes stdin through to the child process so you can type responses.

Getting started

This shipped in azd 1.25.1. Update or install from https://aka.ms/azd:

Terminal window
# Update to the latest
azd update
# Verify you're on 1.25.1+
azd version
# Run something
azd exec python script.py

Official docs are on their way. In the meantime, the azd-exec website has full usage docs, CLI reference, and security guide.

The PR is here if you want to see the full implementation. And the original extension repo has more examples if you want to dig in.

Share:
Share on X