Add `development.exports` in `azure.yaml` for automatic local config sync
- Dominant language
- Go
- Stars
- 569
- Forks
- 364
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 136
Description
## Summary
Issue #4384 highlights a third, related need beyond:
- `env exec` for "run a process with azd env loaded" (#7423)
- `env export` for "manually write selected azd values into app-facing config files" (#7434)
There is also a need for a declarative, automatic way to keep local app config files synced from azd environment values.
The primary scenario here is:
> I want to develop my app locally using azd's provisioned outputs, without rerunning manual export commands after every relevant azd operation
This issue proposes adding a `development.exports` section in `azure.yaml` to describe automatic local config sync behavior for development files.
## Why
Manual `azd env export` is a good first step, but it still requires users or templates to remember to run a command after:
- `azd provision`
- `azd env refresh`
- `azd up`
- switching environments
That creates a predictable failure mode:
- the app's local config file exists
- the azd environment has changed
- the local file is now stale
- the app behaves incorrectly or uses outdated endpoints/settings
For many templates, this is the core user journey:
1. Provision Azure resources with `azd`
2. Export the provisioned outputs into a local app config file
3. Run the app locally against those provisioned resources
If step 2 is always required and easy to forget, template authors will continue to reintroduce custom scripts and special instructions.
This directly addresses the "automatic exports" part of #4384 by making it declarative in `azure.yaml` instead of expecting every template to wire its own scripts.
## What
We should add a configuration surface in `azure.yaml` that describes automatic local sync targets.
Recommended placement:
```yaml
development:
exports:
- out: web/.env.local
include:
- AZURE_CLIENT_ID
- AZURE_OPENAI_ENDPOINT=VITE_AZURE_OPENAI_ENDPOINT
exclude:
- AZURE_ENV_NAME
```
This should also work at service scope:
```yaml
services:
web:
development:
exports:
- out: .env.local
include:
- AZURE_CLIENT_ID=VITE_AZURE_CLIENT_ID
```
Why this placement:
- `development` scales better than `local` if we later add other local-dev concepts
- root-level `development` is the right place for workspace-wide local behavior
- `services..development` gives us a clean service-scoped option without fake services
- `exports` reads naturally as the declarative counterpart to `azd env export`
This should build on the same semantics as the manual export command in #7434 rather than inventing a second model.
## Proposed high-level changes
### 1. Add `development.exports` for root and service scopes
Add a new section in `azure.yaml`:
```yaml
development:
exports:
- out: .env.local
- out: app/.env
include:
- AZURE_CLIENT_ID
- AZURE_TENANT_ID
```
This should work at both scopes:
- `development.exports` for project-root local sync
- `services..development.exports` for service-local sync
Path base should follow scope:
- root-level `development.exports[*].out` is relative to the project root
- service-level `services..development.exports[*].out` is relative to that service path
I would avoid:
- `hooks` — this is declarative state, not imperative scripting
- `pipeline` — wrong scope, since this is not CI/CD configuration
- fake/dummy services — that would overload the meaning of `services`
### 2. Reuse the `env export` model
The configuration shape should align closely with the manual `azd env export` issue (#7434).
That means supporting the same core concepts:
- `out`
- inferred or explicit format
- `include`
- `exclude`
- filtering
- default merge/update semantics
For example:
```yaml
development:
exports:
- out: web/.env.local
include:
- AZURE_CLIENT_ID
- AZURE_OPENAI_ENDPOINT=VITE_AZURE_OPENAI_ENDPOINT
exclude:
- AZURE_ENV_NAME
- out: azd-env.json
include:
- AZURE_OPENAI_ENDPOINT=AzureOpenAI:Endpoint
- AZURE_OPENAI_KEY=AzureOpenAI:Key
```
This keeps the manual and automatic stories consistent:
- command form for one-off/manual export
- declarative form for automatic sync
### 3. Treat this as a sync gesture
This should be a sync gesture, not a narrow "run after these commands" feature.
The principle should be:
> anytime azd updates stored `.env` values in a way that matches the configured export conditions, azd should sync the declared targets
That means we should think in terms of `.env` updates, not derived/defaulted values and not just a hardcoded list of commands.
That said, obvious examples include:
- `azd provision`
- `azd env refresh`
- `azd up`
- `azd env set`
The key principle is:
> if azd updated stored environment values that the app depends on, azd should also sync the declared local targets
### 4. Failure behavior should follow the same spirit as hook-driven local workflows
If sync is part of the configured local development workflow, failures should be surfaced clearly rather than ignored silently.
The working expectation should be similar to how hook failures are treated today: if azd is asked to perform a configured sync step and that step fails, azd should surface it as a real failure.
If multiple sync targets are configured, azd should attempt all of them and then return failure if any target failed.
### 5. Keep it local-only by default
This should be a local-development feature first.
It mutates workspace files, so the default behavior should target interactive/local workflows, not CI.
Manual `azd env export` should still work fine in CI when users want it, but this automatic sync behavior should stay local-only for this issue. I don't think we want a CI override here.
We should support a temporary opt-out via environment variable when users intentionally need to suppress sync for a command or session.
### 6. Preserve existing file-safe semantics
This should inherit the same expectations as `env export`:
- merge/update existing files rather than overwrite wholesale
- preserve unrelated user-managed entries
- leave keys alone if they are no longer selected
- preserve formatting where reasonable
- avoid resolving secret references in v1
Cross-platform defaults should stay simple and predictable:
- write `\n` line endings by default
- keep standard relative path behavior based on scope
- preserve existing file permissions when updating
- use normal user permissions by default when creating new files
If both root and service scopes target the same file, both should still work using normal merge semantics.
Ordering should be:
- root scope first
- service scope second
Successful sync should stay quiet in normal output and only surface detailed activity in debug output. Failures should remain visible.
## Example configuration
### Simple dotenv export
```yaml
development:
exports:
- out: .env.local
```
### Subset selection for a frontend app
```yaml
development:
exports:
- out: web/.env.local
include:
- AZURE_CLIENT_ID=VITE_AZURE_CLIENT_ID
- AZURE_TENANT_ID=VITE_AZURE_TENANT_ID
exclude:
- AZURE_ENV_NAME
```
### Generic JSON for .NET / other tools
```yaml
development:
exports:
- out: azd-env.json
include:
- AZURE_OPENAI_ENDPOINT=AzureOpenAI:Endpoint
- AZURE_OPENAI_KEY=AzureOpenAI:Key
```
This keeps azd focused on producing JSON, while tool-specific consumption can stay outside azd. For example, a .NET workflow could use the JSON output with `dotnet user-secrets set`.
### Service-scoped export
```yaml
services:
web:
development:
exports:
- out: .env.local
include:
- AZURE_CLIENT_ID=VITE_AZURE_CLIENT_ID
- AZURE_TENANT_ID=VITE_AZURE_TENANT_ID
```
## Suggested scope boundaries
In scope for this issue:
- `development.exports` and `services..development.exports` in `azure.yaml`
- automatic sync when azd updates matching `.env` values
- reuse of `env export` concepts such as `include`, `exclude`, filtering, format inference, and merge/update semantics
- local-only defaults
Out of scope for this issue:
- the manual `env export` command itself (#7434)
- the `env exec` command (#7423)
- a broader `development.run` / `azd run` feature
- CI-default or CI-overridable auto-sync behavior
- secret resolution beyond the current env representation
## Why this is worth doing
This completes the workflow split in a clean way:
- #7423: run a process with azd env loaded
- #7434: manually export selected azd values into config files
- this issue: automatically keep declared local config files in sync
That gives template authors a much better answer than custom post-provision scripts, and it makes the "develop locally against azd-provisioned resources" path much more reliable.
Contributor guide
Assessment
This issue has not been assessed yet.