The Azure pack (packs:azure)
packs:azure
The packs:azure entitlement gates opschain-pack-azure, the signed
commercial content pack for Microsoft Azure: an enterprise azure:*
action surface built on the official Azure SDK (azure-sdk-for-go's ARM
runtime) via the pack's own azu tool — never a wrapper around the
vendor CLI. The pack's signed manifest names requires: packs:azure, so
the entitlement in your licence must spell exactly that; like every
packs:<name> grant it is validated by shape and minted per customer
(see entitlements).
The pack mounts like any other action pack — a packs: entry in the
template's .opschain/packs.yaml naming the pack remote and a pinned
revision (opschain docs getting-started/2-4-using-packs) — or installs
into the pack store as a signed bundle. Both doors
enforce the manifest: installing a signed bundle verifies its signature
and its required entitlement, and running any azure:* action without
packs:azure refuses with the named pack refusal before anything dials
Azure.
The shipped actions
azure:api— dial ANY ARM resource provider:method+path(an ARM path, full resource id or operation URL) +api_version+bodyJSON (inline or from a workspace file), with long-running operations drained through the poller (wait,wait_timeout;wait: falsesurfaces anoperation_urlinstead), auto-drained pagination (paginate,max_pages), over-budget results landed as workspace files, and a uniform exit-code taxonomy (usage, validation, not-found, conflict, throttled, authorization).azure:<service>:<client>_<operation>— one generated typed action per SDK operation, 1,700+ across compute, network, storage, resources, sql, keyvault, aks, dns, monitor and authorization, carrying the exact path template, HTTP method and api-version the SDK client dials; long-running methods carrywait/wait_timeout, paged listings carrypaginate/max_pages. The full generated table, including every excluded operation with its reason, ships asCATALOG.mdinside the pack.azure:whoami— resolve the subscription and principal the handed-over credentials dial as. Run it first: a broken credential hand-over fails here, loudly, before anything mutates.azure:vm_run— provision one Linux machine (image urn or id, size, an existing subnet; optional public IP, tags, cloud-init user data). Settles to Succeeded and publishesvm_id,state, addresses — andhost_target(ssh_user@address), ready to hand to the infrastructure pack'shost_registerso the new machine becomes a governed infrastructure host node.azure:vm_state— start / stop / deallocate / restart / delete, settled through the poller;azure:vm_describe— the read half: power state, addresses, size and tags.azure:blob_put/azure:blob_get— stream a workspace file to or from blob storage, bearer-token authorised on the storage scope — no account keys, no SAS tokens in YAML.azure:resource_group_create/azure:resource_group_delete— the deployment boundary, delete drained to completion.azure:deployment_apply— apply an ARM template deployment, drained to a terminal provisioning state; deployment outputs publish asoutput_<name>step outputs.azure:operation_wait— drain anoperation_urlawait: falsedial surfaced earlier.azure:ensure— pre-warm the pinnedazubinary on a runner (source,version,url,sha256; publishesazu_path,azu_version). Never a precondition — see below.
Every step resolves the pinned azu binary itself (PATH, then the
runner cache, then the pack's own bundled tools/ directory, then a
sha256-verified download — never an unpinned public URL). The ensure
step each action carries, and azure:ensure on its own, pre-warm a
runner; neither is a precondition: the platform dispatches step by
step, so sibling steps of one action may execute on different runners
of a group, and a step never trusts a path a sibling published. With no
channel available a step refuses by name, pointing at azure:ensure
and the signed bundle. A pack installed from its signed bundle carries
the tool, so a fresh runner needs no toolchain and no download
configuration.
Worked examples
Each example is a complete declarative action file
(.opschain/actions/<name>.yaml) composing the pack's actions with
uses: + with: (opschain docs orchestration). Nothing secret
appears in any of them — credential custody is server-side (see the
next section) — and subscription_id and location are omitted
wherever the node's azure.subscription_id and azure.location
properties should carry them.
Prove the identity first (azure:whoami)
Open anything that mutates with the liveness probe: a broken hand-over fails here, loudly, and the identity lands as ordinary step outputs.
# .opschain/actions/preflight.yaml
action: preflight
description: Prove the credential hand-over before anything mutates
steps:
- name: identity
uses: azure:whoami
- name: announce
run: echo "dialling subscription ${{ steps.identity.outputs.subscription_id }} as principal ${{ steps.identity.outputs.principal_id }}"
A boundary, a machine, a governed host (azure:resource_group_create, azure:vm_run)
The resource group is the deployment boundary; the machine settles to
Succeeded, and the published host_target (ssh_user@address) seeds
the infrastructure pack's membership hand-over. The SSH public key is a
public value — secrets never ride inputs.
# .opschain/actions/provision_app.yaml
action: provision_app
description: Create the group, provision a machine, register the governed host
inputs:
ssh_public_key: { type: string, required: true } # a public value
subnet_id: { type: string, required: true }
steps:
- name: group
uses: azure:resource_group_create
with: { name: app-dev, tags: service=app }
- name: provision
uses: azure:vm_run
with:
resource_group: app-dev
name: app-vm-01
size: Standard_B2s
image: Canonical:ubuntu-24_04-lts:server:latest
admin_username: opschain
ssh_public_key: '${{ inputs.ssh_public_key }}'
subnet_id: '${{ inputs.subnet_id }}'
- name: register
uses: infrastructure:host_register
with:
parent: /projects/acme/environments/dev
code: app_vm_01
address: '${{ steps.provision.outputs.private_ip }}'
properties: '{"rpc":{"target":"${{ steps.provision.outputs.host_target }}"}}'
Power transitions and the read half (azure:vm_state, azure:vm_describe)
One verb per call — start, stop, deallocate, restart or
delete — settled through the poller; the describe step reads the
settled power state back.
# .opschain/actions/park_app.yaml
action: park_app
description: Deallocate the machine and read its settled power state back
steps:
- name: park
uses: azure:vm_state
with: { resource_group: app-dev, name: app-vm-01, state: deallocate }
- name: read_back
uses: azure:vm_describe
with: { resource_group: app-dev, name: app-vm-01 }
- name: record
run: echo "power state ${{ steps.read_back.outputs.state }}"
Stage artefacts (azure:blob_put / azure:blob_get)
Both stream between the step workspace and blob storage on a bearer token scoped to storage — no account keys, no SAS tokens, nothing in the YAML.
# .opschain/actions/stage_release.yaml
action: stage_release
description: Ship the built artefact out and pull the live manifest in
steps:
- name: build
run: tar -czf release.tar.gz build/
- name: ship
uses: azure:blob_put
with: { account: acmereleases, container: web, blob: release.tar.gz, file: release.tar.gz }
- name: manifest
uses: azure:blob_get
with: { account: acmereleases, container: web, blob: manifest.json, file: manifest.json }
- name: record
run: echo "shipped etag ${{ steps.ship.outputs.etag }} (${{ steps.ship.outputs.bytes }} bytes)"
ARM template deployments (azure:deployment_apply)
template and parameters take JSON inline or as @file: workspace
references; the deployment drains to a terminal provisioning state and
its template outputs publish as output_<name> step outputs — a
template output named subnetId lands as output_subnet_id.
# .opschain/actions/apply_network.yaml
action: apply_network
description: Apply the network template and consume its outputs
steps:
- name: apply
uses: azure:deployment_apply
with:
resource_group: app-dev
name: network
template: '@file:templates/network.json'
parameters: '@file:templates/network.dev.json'
- name: record
run: echo "deployment ${{ steps.apply.outputs.provisioning_state }}; subnet ${{ steps.apply.outputs.output_subnet_id }}"
The full-coverage lane (azure:api)
Any ARM resource provider by path + api_version (method defaults
to GET): each top-level response field publishes as a snake_case step
output, and a list response auto-drains its nextLink up to
max_pages.
# .opschain/actions/inspect_vm.yaml
action: inspect_vm
description: Read the machine's model and instance view in one dial
steps:
- name: view
uses: azure:api
with:
path: /resourceGroups/app-dev/providers/Microsoft.Compute/virtualMachines/app-vm-01
api_version: "2025-04-01"
query: '{"$expand":"instanceView"}'
A long-running dial drains through the poller by default; wait: "false" surfaces operation_url instead, and azure:operation_wait
settles it later — kick work off, do other work, then drain.
# .opschain/actions/rolling_restart.yaml
action: rolling_restart
description: Kick a restart, then settle the operation when it matters
steps:
- name: kick
uses: azure:api
with:
method: POST
path: /resourceGroups/app-dev/providers/Microsoft.Compute/virtualMachines/app-vm-01/restart
api_version: "2025-04-01"
wait: "false"
- name: drain
uses: azure:operation_wait
with:
url: '${{ steps.kick.outputs.operation_url }}'
A generated typed action (azure:compute:virtual_machines_instance_view)
Every generated action is the same lane with the SDK operation's path parameters as typed inputs, carrying the exact path template, HTTP method and api-version the SDK client dials.
# .opschain/actions/instance_view.yaml
action: instance_view
description: Read the run-time state of a machine over the typed surface
steps:
- name: view
uses: azure:compute:virtual_machines_instance_view
with:
resource_group_name: app-dev
vm_name: app-vm-01
Close the boundary (azure:resource_group_delete)
Destructive and drained to completion — everything inside the group goes with it.
# .opschain/actions/tear_down.yaml
action: tear_down
description: Delete the group and everything in it
steps:
- name: delete
uses: azure:resource_group_delete
with: { name: app-dev }
Credentials custody
Reference-first: secrets are custodied as vault references and resolved
by the platform into masked environment variables (tenant, client id,
client secret, certificate or federated-token variables), or the runner
carries workload or managed identity and no variables at all. azu
dials only the credential that is configured: when a client secret,
certificate (file or inline) or federated token file is set, that one
mode is built through its own constructor and a mode that cannot be
built refuses by name with the cause — it never falls through to
workload or managed identity. The ambient chain (workload identity, then
managed identity) is used only when no explicit variable is set. Each
dial logs one value-free line naming which credential channels were
present (kind and length only). An
unresolved vault reference refuses the dial naming the variable — the
value is never echoed. Every action resolves the subscription from the
node's azure.subscription_id property unless overridden in with:,
and azure.location seeds locations the same way.
Entitlement and lapse behaviour
The signed-pack gate, uniformly: a LimePoint-signed pack whose verified
manifest requires packs:azure is gated on that feature at install and
at every action run — the refusal names the pack, the mount and the
missing entitlement, and lands a customer-visible licence warning event.
The grant is layered by a pack-tier licence document beside your base
licence and stops contributing once that document expires past its grace
window: installed content and stored configuration are never removed,
reads stay up, and everything lights back up when a renewed licence is
applied.
Composition with other packs
- The infrastructure pack closes the loop:
azure:vm_runpublisheshost_target, whichinfrastructure:host_register(packs:infrastructure) turns into a registered, governed infrastructure host node — provision on Azure, then operate over the brokered transport family. - The AWS sibling is the AWS pack (
packs:aws): the same shape — a governedapilane, a generated typed catalog and provisioning hand-over — for Amazon Web Services.
Where the rest lives
- Entitlements — the pack vocabulary and refusal shapes.
- The pack store — installing signed bundles.
- The infrastructure pack — registering provisioned machines as governed hosts.
opschain docs getting-started/2-4-using-packs— mounting a pack onto a template.