Skip to main content

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.login times out after 5 minutes.
  • state mismatch: … vs None means an old localhost:8400 tab (a previous "Authentication complete" page, reloaded or restored) answered first. Close every such tab and run azure.login again.

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)

Source distribution for make-azure 0.3.0
File Size Uploaded
make_azure-0.3.0.tar.gz 22.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for make-azure 0.3.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page