make-azure
A virtual machine on Azure's free tier, for mkrun:
signed in with Microsoft's own azure-identity, created with every field that
decides the bill set explicitly, and removed with nothing billed left behind.
No az CLI.
# Makefile.py in a consuming repo
# /// script
# requires-python = ">=3.11"
# dependencies = ["mkrun>=0.4.1", "make-azure>=0.2"]
# ///
from make_azure import azure # importing is what registers the group
from make_azure.vm import Azure
Azure.configure(tenant="contoso.onmicrosoft.com", location="swedencentral") # optional
$ mk azure.login --tenant <id or domain> # once; a browser opens
$ mk azure.login --tenant <t> --device-code # no browser here: enter a code elsewhere
$ mk azure.account # which subscription, and is it a free one
$ mk azure.vm box # Ubuntu 24.04 on a B2ats_v2, SSH open
$ mk azure.vm arm --size Standard_B2pts_v2
$ mk azure.vms # what the subscription has
$ mk azure.delete box # the VM and everything it made
$ mk -n azure.vm box # the plan; nothing is sent, no sign-in needed
Signing in
azure.login runs the sign-in inside its own process, so the browser's
redirect goes back to the process that is waiting for it. It keeps two things:
- an authentication record (account, tenant, username; no secret) in
~/.make/azure/<tenant>.json, which says which account later calls use; - the tokens, in MSAL's encrypted cache: the macOS Keychain, or libsecret on Linux. Unencrypted storage is refused, never fallen back to.
Every later task gets its token silently from those two. It never opens a
browser in the middle of azure.vm: an expired sign-in is an error that says
mk azure.login.
In CI, an identity in the environment wins: AZURE_CLIENT_ID + AZURE_TENANT_ID
with AZURE_FEDERATED_TOKEN_FILE (workload identity) or AZURE_CLIENT_SECRET.
The secret's name marks it as a credential, so mkrun withholds it from child
processes and redacts it.
⚠ There is no az fallback, on purpose. One address can be both a personal
Microsoft account and a work account, and silently reusing whichever az
session is around is how the wrong one gets used.
⚠ MFA is asked for at sign-in, on purpose. Since Azure's mandatory MFA
(2025), Resource Manager answers any create, update or delete made without it
with 401 RequestDisallowedByAzure and a claims challenge (acrs: p1). Reads
still pass, so everything up to the first write would work and then fail.
azure.login asks for those claims up front, and arm answers a challenge
silently if one still comes. The user does MFA once, at sign-in, as the portal
makes them.
⚠ School and work tenants often block --device-code. Microsoft's managed
Conditional Access policy against device-code phishing answers
AADSTS53003 "does not meet the criteria" after a successful sign-in, and
the device-code flow then polls until its code expires (about 15 minutes):
Ctrl-C. Use the browser flow, which the same policy allows. Measured on a
school tenant (Azure for Students), 2026-10-07: device code refused, browser
admitted, VM created, SSH in, deleted.
Two browser-flow traps, both with a fix on screen:
- The redirect has to reach this process. The sign-in URL is printed as
well as opened. Open it in the browser you actually sign in with; if you sign
in in some other tab, nothing comes back and
azure.logintimes out after 5 minutes. state mismatch: … vs Nonemeans an oldlocalhost:8400tab (a previous "Authentication complete" page, reloaded or restored) answered first. Close every such tab and runazure.loginagain.
azure-identity signs in as the Azure CLI's public client
(04b07795-8ddb-461a-bbee-02f9e1bf7b46), so a tenant that blocks az outright
blocks this too.
What "free" means here
An Azure free account gives, for its first 12 months, 750 hours a month of
each of Standard_B1s, Standard_B2ats_v2 (AMD) and Standard_B2pts_v2 (Arm).
That's one VM running all month. It also gives two 64 GiB P6 managed disks.
Azure for Students carries the same free services. Everything else bills:
a portal or az vm create default |
here | |
|---|---|---|
| OS disk | 30 GiB → billed as P4, a meter the offer does not cover | 64 GiB Premium SSD = P6, the free one |
| size | Standard_DS1_v2 and friends |
Standard_B2ats_v2; anything outside the three is refused |
| resource group | one you name, shared | <name>-rg, one per VM, so delete takes all of it |
| public IP | Standard static IPv4 | the same, and it is billed (about $3.65 a month) |
⚠ The public IPv4 is the one cost. Basic public IPs, which the free account
used to cover, were retired on 2025-09-30, and a Standard IPv4 bills by the hour
whether or not the VM runs. --no-public-ip leaves it out; the VM is then
reachable only from inside its network.
⚠ A pay-as-you-go subscription takes the same request and pays for it.
azure.vm reads the subscription's offer (quotaId) and refuses anything but
a free account or Azure for Students. A free account upgraded to pay-as-you-go
keeps its free hours until month 12 but reports the paid offer; --paid-ok is
for that case.
It also refuses, before creating anything: a sign-in that reaches no
subscription, or several when none is named; a region that will not sell the
size to this subscription (free accounts are often restricted in busy regions,
so try another --location); and a name already taken. A second VM of the
same size is allowed, with a warning: two running all month exceed the 750 hours.
How it talks to Azure
Plain REST to Resource Manager through make.http, with a bearer token from
azure-identity. No azure-mgmt-* SDKs. The VM is one template
deployment (template.py): network, NSG, IP, NIC and VM in a single request.
Azure works out the order, and a failure reports Azure's own reason
(SkuNotAvailable: …). azure-identity is imported only when a token is
needed, so mk --list never pays its import time.
Settings
Azure.configure(...) in the task file, an [azure] table in the config file,
or MAKE_AZURE_<FIELD> in the environment:
| field | default | |
|---|---|---|
tenant |
the only one azure.login saved |
tenant id or domain |
subscription |
the only one the sign-in reaches | id or display name |
location |
westeurope |
any region with B-series capacity |
size |
Standard_B2ats_v2 |
one of the three free sizes (below) |
admin |
azureuser |
the login user |
ssh_key |
~/.ssh/id_ed25519.pub, then id_rsa.pub |
the public key the VM accepts |
Which free size. All three have run; the two v2 sizes were measured on 2026-10-07 (Azure for Students, westeurope,
one VM of each --size; Ubuntu 24.04, Python 3.12) and the default is the AMD one:
Standard_B2ats_v2 (AMD EPYC 7763) |
Standard_B2pts_v2 (Arm Neoverse-N1) |
|
|---|---|---|
| Python, one core | 2.18 s | 3.05 s |
| two jobs, both cores | 2.12 s (no speed-up: one core, two threads) | 1.59 s |
| SHA-256 / AES-GCM | 1.57 / 4.27 GB/s | 1.87 / 2.98 GB/s |
| disk (P6) write / read | 133 / 132 MB/s | 96 / 96 MB/s |
| memory left to you | 842 MiB | 950 MiB |
Pick the Arm size for builds that keep two cores busy, if everything you install has an arm64
build. Standard_B1s is one older vCPU and is kept only because it is free.
The group is azure, and it merges with a repo's own azure.* tasks; this
package claims login, account, vm, vms and delete, nothing else.
Metadata
Release files for make-azure 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| make_azure-0.3.0.tar.gz | 22.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| make_azure-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 43.8 kB
Release files / make_azure-0.3.0.tar.gz
| Download URL | make_azure-0.3.0.tar.gz |
|---|---|
| Size | 22.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
be812cb5b946b477038a3098d003d3c05b46773d87f8ae54bd47ddd09d3caace
|
|
BLAKE2b-256 checksum How to use checksums |
46078a7ae83be454a32d8400344783ea9e57b01974938607b723324f123a1a47
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / make_azure-0.3.0-py3-none-any.whl
| Download URL | make_azure-0.3.0-py3-none-any.whl |
|---|---|
| Size | 21.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8546f3313868a764a4d02572224b6ae14ae65821edeff0f69a73329ca8637d61
|
|
BLAKE2b-256 checksum How to use checksums |
12fb6a4942e3fea053e3446f2f474aa41cf11483bc0c14bde506e7c036c06b0c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|