Setup Guide -- azemu¶
Everything required to get azemu running and talking to Terraform.
Prerequisites¶
- Docker and Docker Compose (for the quick-start path)
- flox (for the contributor dev environment; pulls Go, Terraform, etc.)
- macOS or Linux
Docker (quick start)¶
The fastest way to get azemu running. No Go toolchain, no flox, no manual cert trust.
This builds the image, starts azemu, and exposes three ports:
| Port | Protocol | Purpose |
|---|---|---|
| 4566 | HTTPS | ARM API |
| 4567 | HTTPS | Metadata, OAuth2, OIDC |
| 4568 | HTTP | Health check (no TLS) |
The compose file bind-mounts .azemu/ from the host, so the self-signed cert
bundle appears at .azemu/cert-bundle.pem on the host after first boot.
To run Terraform against azemu:
export SSL_CERT_FILE=$PWD/.azemu/cert-bundle.pem
cd examples/terraform
terraform init && terraform apply -auto-approve
Or use the scripts/aztf wrapper which handles the env-var exports and
starts azemu automatically:
./scripts/aztf -chdir=examples/terraform init
./scripts/aztf -chdir=examples/terraform apply -auto-approve
To stop:
Development environment (flox)¶
The repo ships a fully pinned .flox/env/manifest.toml. Activating it gives
you Go, Terraform ^1.14, pre-commit, jq, just, shellcheck and tflint at
exactly the versions the project is tested against. You do not need to install
any of these system-wide.
The activation hook runs once per environment and:
- Creates
.azemu/for the persistent TLS cert bundle. - Installs
.git/hooks/pre-commitfrom.pre-commit-config.yamlif it isn't already. - Reports if azemu is already running with a persistent cert.
Helper functions provided by the profile:
| Function | What it does |
|---|---|
azemu-start |
Builds, starts the binary, prints the one-time cert-trust command, probes /metadata/endpoints |
azemu-stop |
pkill -f bin/azemu |
azemu-status |
Reports running/stopped and dumps the name and resourceManager fields from /metadata/endpoints |
azemu-smoke |
Inline smoke test against a running instance |
tf-init, tf-plan, tf-apply, tf-destroy |
terraform -chdir=$TF_DIR ... against azemu (with azemu-status precheck) |
Aliases: ti, tp, ta, td, ts |
Short forms of the above |
Environment variables¶
Sourced from .flox/env/manifest.toml [vars] and pkg/config/config.go:
| Variable | Required | Default | Purpose |
|---|---|---|---|
AZEMU_ARM_PORT |
No | 4566 |
ARM HTTPS port (informational; binary is hard-coded today) |
AZEMU_META_PORT |
No | 4567 |
Metadata HTTPS port (informational) |
AZEMU_CERT_PATH |
No | unset | When set, persist the self-signed cert+key as a PEM bundle at this path; trust once and restart freely. When unset, a fresh cert is generated and written to OS temp on every startup. |
AZEMU_AZURITE_ENDPOINT |
No | http://azurite:10000 |
Blob service base URL for the Azurite sidecar. azemu derives queue (port 10001) and table (port 10002) endpoints from this. Set to http://localhost:10000 when running Azurite directly on the host. |
AZEMU_REDIS_ENDPOINT |
No | redis://azemu-redis:6379 |
Connection URL for the Redis sidecar. azemu derives the hostName field on Microsoft.Cache/Redis responses from the URL host. Set to redis://localhost:6379 when running Redis directly on the host. |
AZEMU_SUBSCRIPTION_ID |
No | 00000000-0000-0000-0000-000000000000 |
Mock subscription returned by ARM list endpoints |
AZEMU_TENANT_ID |
No | 00000000-0000-0000-0000-000000000001 |
Mock tenant returned by token / OIDC endpoints |
AZEMU_METADATA_HOST |
No | localhost:4567 |
Host substituted into URLs in /metadata/endpoints |
ARM_METADATA_HOSTNAME |
Yes (Terraform) | flox sets 127.0.0.1:4567 |
Tells azurerm to discover endpoints via azemu instead of real Azure |
ARM_SUBSCRIPTION_ID / ARM_TENANT_ID / ARM_CLIENT_ID / ARM_CLIENT_SECRET |
Yes (Terraform) | flox sets all four | Mock credentials; azemu accepts any value |
TF_DIR |
No | test/terraform |
Working directory for the tf-* profile aliases |
Storage and Azurite¶
azemu owns the ARM management plane for Microsoft.Storage/storageAccounts and
Microsoft.Storage/storageAccounts/blobServices/containers. The Storage data
plane (blob upload/download, queue messages, table rows) is delegated to
Azurite, Microsoft's official Azure Storage
emulator.
When using docker compose up, the azurite service starts automatically
alongside azemu. azemu returns path-style Azurite endpoint URLs in the
primaryEndpoints block of every storage account response, and the listKeys
endpoint returns Azurite's well-known development key. SDK clients that are
pointed at these endpoints can authenticate against Azurite without any
extra configuration.
Storage account names and AZURITE_ACCOUNTS¶
Azurite only serves accounts it knows about; by default that is
devstoreaccount1, and any other account name is rejected even with the
correct key. azemu hands out endpoints of the form
http://azurite:10000/{accountName} for whatever account name your
Terraform chooses, so each Terraform-created account name must be
registered with Azurite via the AZURITE_ACCOUNTS environment variable on
the azurite service:
Rules:
- Reuse the well-known development key for every entry. azemu's
listKeysreturns that key for any account, so SDK clients authenticate without extra configuration. - Keep
devstoreaccount1in the list. SettingAZURITE_ACCOUNTSdisables the built-in default account unless it is listed explicitly. - Append one
;{name}:{key}entry per Terraform storage account. Azurite re-reads the variable about once a minute;docker compose restart azuriteapplies it immediately.
docker-compose.yml ships with devstoreaccount1, examplestorage001
(used by examples/terraform/), and azemuotasa (used by the
ota-updates scenario) pre-registered.
Note on containers: azurerm_storage_container resources exist in azemu's
ARM store only and are not mirrored into Azurite. Create the container in
Azurite from your upload script before the first blob write (one Create
Container call, optionally with x-ms-blob-public-access: blob for
anonymous read access).
When running azemu directly on the host (outside Docker):
- Start Azurite:
docker run -d -p 10000:10000 -p 10001:10001 -p 10002:10002 \
mcr.microsoft.com/azure-storage/azurite \
azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0
- Point azemu at the local instance:
Azurite ports:
| Port | Service |
|---|---|
| 10000 | Blob |
| 10001 | Queue |
| 10002 | Table |
Redis sidecar (optional)¶
azemu owns the ARM management plane for Microsoft.Cache/Redis. The Redis
data plane (RESP protocol on port 6379) is delegated to the upstream
redis container. Per design note 3, this
mirrors the Azurite delegation pattern, azemu serves the management surface
and the canonical implementation handles the data plane.
The sidecar is opt-in via a docker compose profile so default users do not pay the startup cost when they are not exercising Redis:
The Redis service binds to host port 6379, runs redis-server with
--requirepass azemu-dev-primary-key, and exposes a redis-cli ping
healthcheck. The password value matches what azemu's listKeys endpoint
returns for azurerm_redis_cache.example, so an SDK client that reads its
connection key from the ARM response authenticates against the sidecar
without any further configuration:
When running azemu directly on the host (outside Docker), start Redis yourself and point azemu at it:
docker run -d -p 6379:6379 --name azemu-redis \
redis:7-alpine redis-server --requirepass azemu-dev-primary-key
export AZEMU_REDIS_ENDPOINT=redis://localhost:6379
./bin/azemu
The deterministic listKeys contract is documented in
design note 3.
Premium-tier features (clustering, persistence, geo-replication,
regenerateKey) are out of scope for the initial implementation, see the
Parity Matrix for the follow-up list.
TLS certificate trust¶
azemu serves both ports over HTTPS using a self-signed ECDSA P-256 certificate
with SANs for localhost and 127.0.0.1. There are two modes.
Persistent (recommended)¶
Set AZEMU_CERT_PATH to a stable PEM bundle file. azemu loads the cert+key
from there on startup, or generates and writes a fresh pair (mode 0600) if
the file does not exist or fails validation. The flox profile defaults this to
.azemu/cert-bundle.pem and the directory is gitignored.
Trust the bundle once in the system keychain -- subsequent restarts reuse the same cert and keychain prompt does not return:
# macOS -- TouchID/password prompt fires once
security add-trusted-cert -r trustRoot -p ssl \
-k ~/Library/Keychains/login.keychain-db \
.azemu/cert-bundle.pem
# Linux
sudo cp .azemu/cert-bundle.pem /usr/local/share/ca-certificates/azemu.crt
sudo update-ca-certificates
azemu-start (provided by the flox profile) prints the exact macOS command on
first run, scoped to your bundle path.
Ephemeral (legacy)¶
If AZEMU_CERT_PATH is unset, azemu generates a fresh cert on every startup
and writes a cert-only file to OS temp. The path is logged on startup:
You must re-run security add-trusted-cert after every restart in this mode,
because the Go-based azurerm provider checks the macOS keychain (it ignores
SSL_CERT_FILE). This is why the persistent mode exists; prefer it.
Terraform provider configuration¶
The provider must use metadata_host so it discovers azemu's endpoints instead
of Azure's public cloud URLs.
provider "azurerm" {
features {}
metadata_host = "127.0.0.1:4567"
resource_provider_registrations = "none"
subscription_id = "00000000-0000-0000-0000-000000000000"
tenant_id = "00000000-0000-0000-0000-000000000001"
client_id = "00000000-0000-0000-0000-000000000002"
client_secret = "azemu-mock-secret"
}
Environment variable alternative (the flox profile exports these for you):
export ARM_METADATA_HOSTNAME=127.0.0.1:4567
export ARM_SUBSCRIPTION_ID=00000000-0000-0000-0000-000000000000
export ARM_TENANT_ID=00000000-0000-0000-0000-000000000001
export ARM_CLIENT_ID=00000000-0000-0000-0000-000000000002
export ARM_CLIENT_SECRET=azemu-mock-secret
Use
127.0.0.1, notlocalhost. macOS resolveslocalhostto::1first and azemu listens on IPv4 -- Terraform will fail withdial tcp [::1]:4567: connection refusedotherwise. Also note thatskip_provider_registrationis deprecated in azurerm v4.x and silently ignored; use theresource_provider_registrationsform above.
Metadata cloud classification¶
The azurerm provider classifies clouds by inspecting the /metadata/endpoints
response. If the resourceManager URL uses http:// instead of https://, the
provider classifies the environment as Azure Stack and refuses to connect. azemu
serves ARM endpoints on HTTPS to avoid this rejection.
Running the server¶
make build
./bin/azemu # ephemeral cert
# or
mkdir -p .azemu
AZEMU_CERT_PATH=$PWD/.azemu/cert-bundle.pem ./bin/azemu # persistent cert
Ports:
:4566(HTTPS) -- ARM API, data plane:4567(HTTPS) -- metadata service, OAuth2, OIDC
Available make targets¶
Sourced from Makefile:
| Target | Description |
|---|---|
make build |
go build -o bin/azemu ./cmd/azemu (with -ldflags version) |
make run |
make build && ./bin/azemu |
make test |
go test ./... -v -count=1 |
make coverage |
Run tests with coverage, generate coverage.html |
make smoke |
Build, start server, run inline curl smoke test, stop server |
make docker |
Build the Docker image as azemu:latest |
make docker-run |
Build and run the image with ports 4566/4567/4568 exposed |
make docker-compose |
docker compose up -d --build |
make docker-compose-down |
docker compose down -v |
make tf-test |
Run terraform test in examples/terraform/ |
make clean |
Remove bin/, coverage.out, coverage.html |
Quick validation¶
# Metadata endpoint
curl -sk https://127.0.0.1:4567/metadata/endpoints?api-version=2022-09-01
# ARM subscriptions
curl -sk https://127.0.0.1:4566/subscriptions?api-version=2022-12-01
# Full automated smoke test
make smoke
# End-to-end against the real azurerm provider
ta && td # tf-apply && tf-destroy (flox aliases)