Deploy: Templates
A deployment template describes DataStream resources in Bicep syntax. Deploying it creates what is missing and updates what differs; it never deletes anything. Templates deploy from the console (Deploy) and from the public REST API (Deployments API), and any existing resource can be exported as a template to deploy elsewhere.
The language is a subset of Azure Bicep. Everything described here parses the way Bicep parses it, so a template opens cleanly in the Bicep extension for VS Code, which does not know the VirtualMetric resource types and says so.
How a Deployment Works
A resource is identified by its name. Each resource declaration names its type and version, and its name. Deploying looks the resource up by that name (a child resource by its name within its parent): when it does not exist it is created; when it exists its properties are compared with the template's and it is updated only if they differ. A resource the template does not mention is never touched, and nothing is ever deleted: this is Azure's incremental mode.
resource director 'VirtualMetric/directors@2026-09-27' existing = {
name: 'director-01'
}
resource syslog 'VirtualMetric/devices@2026-09-27' = {
name: 'syslog-udp'
properties: {
type: 'syslog'
relations: [
{
id: director.id
type: 'director'
}
]
properties: {
protocol: 'udp'
port: 514
}
}
}
properties is the console's request body for that resource, without its name: what the console sends when you create or edit the resource. One convenience everywhere: a field the API carries as a JSON string (a device's, target's, director's or cluster's own properties) is a plain object in a template.
existing resources are references. A resource declared existing is only looked up, never written, so the template can refer to something it does not manage. The deployment fails if it does not exist.
param directorName string = 'director-01'
resource director 'VirtualMetric/directors@2026-09-27' existing = {
name: directorName
}
References are ids. Where the API takes the id of another resource, a template refers to that resource: director.id, pipeline.id. Referring to a resource makes the deployment wait for it, exactly as in Bicep, so resources deploy in an order that works whatever order they are written in. Together with existing, this is what makes a template portable: the director is found by name in whichever tenant the template deploys to, and its id there is used.
Secrets never leave the backend. Reading a resource withholds its secrets, and writing one with an empty secret keeps the stored value. An exported template therefore declares each secret as a @secure() parameter whose empty default keeps what is stored; deploying the template to a new tenant is when you fill it in. A secure parameter's value is never echoed, logged, audited or stored with the deployment: it shows as ***, and so does a secure value inside another parameter's value ('Password=${pw}').
A secret must come through a @secure() parameter. A value at a property the resource type treats as a secret (a password, a client secret, a token, a vault secret's value) must be a @secure() parameter, or contain one: written into the template, or passed through a plain parameter, it would be stored with the deployment and shown to everyone who can read it. Such a template is refused with VM042, at the value. An empty value is fine: it keeps the stored secret. A value the template learns only as it runs is checked before its resource is written. *** itself is refused as a secure parameter's value (VM031): it is what the deployment shows in place of every secure value, so sending a stored deployment's parameters back would deploy the marker as the secret.
What-if reports what a deployment would do, writing nothing: for each resource whether it would be created, modified (with each changed property, before and after), left unchanged, read (existing), run (an action), ignored (a false condition), or unknown when it cannot tell (a lookup failed, or the name depends on another resource), with the reason. A value the deployment would learn only as it runs, such as the id of a resource it would create, shows as (known after deploy).
A deployment runs one resource at a time, in dependency order, and stops at the first failure; the resources after it are reported as skipped. Resources already written stay written: run the deployment again once the cause is fixed, and what already matches reports no change.
Lookups run before the first write. Before anything is written, the deployment looks up every resource it can already name: each one whose name is known before the deployment starts and whose parent is none, an existing resource, or a resource that already exists. A refusal a resource type makes at lookup (a pipeline name reserved by a pending change, an agent, a name two resources share) then fails the deployment with nothing written, instead of halfway through: the failed lookup is reported on its resource, and every other resource is skipped. The children of a parent the deployment creates, and actions, are not looked up then. Each resource is still looked up again as its turn comes.
A change a resource cannot take is refused before the first write. Some properties are fixed once a resource exists: a director's mode, containerType, processingMode and stage, a device's and a cluster's type, a dataset's type and definitionType, a Library sample's kind, a Content Hub route's pack, and a quick route's deviceId and targetId. Nor can a template write an agent, write a staged pipeline's main (stage: 'main'), or move a vault secret into the VirtualMetric vault without its value. The what-if shows such a change as it is (modify, with its delta) and reports it with VM043, at the property. A deployment checks it with the lookups above and refuses it before anything is written: that resource's operation fails with DEPLOYMENT_TEMPLATE_INVALID, and every other one is skipped. The ids of the resources the lookups found count as known, so a device naming an existing director by director.id is checked then. A resource whose properties are known only as the deployment runs (they come from a resource it creates, for instance) is checked when its turn comes, and refused then.
A resource is declared once. Two resources, or two items of a loop, that would deploy the same resource (the same type, under the same parent, with the same name) are refused with VM025, naming both: the second would find what the first wrote and overwrite it. When the names are known only as the deployment runs, the second one fails instead of writing.
A deployment runs for at most 30 minutes. Past that, the resource running is left to finish if it can, the rest are skipped, and the deployment fails with DEPLOYMENT_TIMED_OUT. Run it again: what already matches reports no change.
Permissions are checked first. Before anything is read or written, the deployment checks that you hold what every resource needs: to read, create and update a resource (a deployment reads it to report what changes), to read an existing one, and the features of your edition. If anything is missing, the deployment is refused with the whole list, and nothing happens. A what-if needs the read permissions and the features only. Some types ask for less when the template asks for less: a pipeline with review: true does not need PIPELINE_COMMIT_MERGE, and a staging sync needs PIPELINE_DELETE only with endStaging: true. That holds when the property is written out in the template, not read from another resource.
A deployment shows each resource only to readers of its type. Anyone with DEPLOYMENT_READ sees the organization's deployments, but a resource of a type you cannot read shows as restricted: its name, id, changes, warnings and outputs are hidden, and its error keeps its code with the message Details are hidden: reading <type> needs <KEY>. A deployment with any such resource also hides its template, parameters and outputs, and lists the read permissions you lack.
The Language
A template is a sequence of declarations, one per line; their order does not matter. Comments are // to the end of the line and /* between markers */.
targetScope = 'tenant'
metadata description = 'A syslog listener on a director'
@description('The device name')
param deviceName string = 'syslog-udp'
var port = 514
resource director 'VirtualMetric/directors@2026-09-27' existing = {
name: 'director-01'
}
resource syslog 'VirtualMetric/devices@2026-09-27' = {
name: deviceName
properties: {
type: 'syslog'
relations: [
{
id: director.id
type: 'director'
}
]
properties: {
protocol: 'udp'
port: port
}
}
}
output deviceId string = syslog.id
Declarations
| Declaration | Syntax | Notes |
|---|---|---|
| target scope | targetScope = 'tenant' | Optional. Only 'tenant' is accepted. |
| metadata | metadata <name> = <value> | Kept with the deployment; the value must be a constant. |
| parameter | param <name> <type> [= <default>] | Supplied when deploying; see Parameters. |
| variable | var <name> = <expression> | Evaluated once, when first used. |
| resource | resource <symbol> '<Type>@<version>' [existing] = <body> | See Resources. |
| output | output <name> <type> = <expression> | Evaluated after the deployment. |
| type | type <name> = <type> | A user-defined type for parameters. |
func, module, import, using and extension are Bicep features VirtualMetric templates do not support; a template that uses one is refused with a diagnostic saying so.
Parameters
A parameter's type is string, int, bool, object, array, a user-defined type, or an inline type: a union of literals ('udp' | 'tcp'), an array (string[]), an object ({ host: string, port: int? }), or a nullable type (string?). secureString and secureObject are accepted as @secure() string and @secure() object.
A parameter without a default must be supplied, unless its type is nullable (then it is null). A default may refer to other parameters, and only a default may call newGuid() and utcNow(). A default may read tenant(), but not deployment(): the parameters are bound before the deployment exists.
@description('The listener port')
@minValue(1)
@maxValue(65535)
param port int = 514
@allowed([
'udp'
'tcp'
])
param protocol string = 'udp'
param mode 'managed' | 'serverless' = 'managed'
@secure()
param token string = ''
@metadata({ picker: 'director' })
param directorName string
param label string = '${protocol}-${port}'
| Decorator | On | Meaning |
|---|---|---|
@description('...') | parameter, variable, resource, output, type | Shown in the parameters form. |
@allowed([...]) | parameter | The value must be one of the list; the form shows a dropdown. For an array parameter, every item must be. |
@minValue(n), @maxValue(n) | int parameter | The range. |
@minLength(n), @maxLength(n) | string or array parameter | The length. |
@secure() | string or object parameter, output | Never echoed, logged or stored: shown as ***. |
@metadata({...}) | parameter, resource, output | Free-form. On a string parameter, picker makes the form offer existing resources: director, cluster, device, target, pipeline, advancedRoute, dataset, profile, vaultSecret. The value stays the resource's name. |
@batchSize(n) | resource loop | Accepted; resources deploy one at a time anyway. |
Every decorator can also be written with its namespace: @sys.description('...'). A decorator's arguments must be constants.
Resources
A resource's body is an object with its name, its properties, and optionally parent and dependsOn. The type string is '<Type>@<version>'; every type's version today is 2026-09-27.
resource director 'VirtualMetric/directors@2026-09-27' existing = {
name: 'director-01'
}
resource syslog 'VirtualMetric/devices@2026-09-27' = {
name: 'syslog-udp'
properties: {
type: 'syslog'
relations: [
{
id: director.id
type: 'director'
}
]
}
}
resource archive 'VirtualMetric/devices@2026-09-27' = {
name: 'archive'
properties: {
type: 'syslog'
relations: [
{
id: director.id
type: 'director'
}
]
}
dependsOn: [
syslog
]
}
- References.
<symbol>.id,.name,.type,.apiVersionand.properties.<path>read a resource once it is deployed (or looked up)..idis the backend id as a string, as every API sends ids. Reading any of them makes the deployment wait for that resource;dependsOnadds a wait the properties do not show. - Conditions.
= if (<condition>) { ... }deploys the resource only when the condition is true. Reading a resource that did not deploy is an error, unless the access is safe:res.?idisnullfor it. - Loops.
= [for <item> in <array>: { ... }], or[for (<item>, <index>) in <array>: { ... }], deploys the body once per item; each deployment is namedsymbol[0],symbol[1], and so on, andsymbol[i].idreads one of them. A loop may filter:[for x in xs: if (<condition>) { ... }]. A loop's array must be known when the deployment starts: it may use parameters and variables, not the properties of other resources. - Child resources (a pipeline's child pipelines, its staging clone) are named within their parent. Declare them inside the parent's body, where their type is relative to the parent's and their version defaults to the parent's, or at the top level with
parent:.
param count int = 2
resource director 'VirtualMetric/directors@2026-09-27' existing = {
name: 'director-01'
}
resource pipeline 'VirtualMetric/pipelines@2026-09-27' = {
name: 'firewall_logs'
properties: {
content: '''
processors:
- set:
field: source
value: firewall
'''
}
resource normalize 'children' = {
name: 'normalize'
properties: {
content: 'processors: []'
}
}
}
resource staging 'VirtualMetric/pipelines/stagings@2026-09-27' = {
parent: pipeline
name: 'staging'
properties: {}
}
resource listeners 'VirtualMetric/devices@2026-09-27' = [for i in range(0, count): {
name: 'listener-${i}'
properties: {
type: 'syslog'
pipelineId: pipeline.id
relations: [
{
id: director.id
type: 'director'
}
]
}
}]
output childId string = pipeline::normalize.id
output firstListener string = listeners[0].id
A nested resource is referred to from outside its parent as parent::child, and deploys only when its parent does.
Expressions
| Kind | Syntax |
|---|---|
| Literals | 'single-quoted strings', integers (64-bit), true, false, null |
| Arrays | [1, 2], or one item per line |
| Objects | { key: value }, or one property per line; keys are identifiers or quoted strings |
| Interpolation | '${prefix}-${port}' |
| Multi-line strings | '''...''': verbatim, no escapes, no interpolation; a line break right after the opening quotes is not part of the value |
| Escapes | \' \\ \n \r \t \$ \u{1F600} |
Operators, loosest first:
| Operators | Meaning |
|---|---|
c ? a : b | ternary; only the chosen branch is evaluated |
a ?? b | a unless it is null |
a || b | or, stopping at the first true |
a && b | and, stopping at the first false |
== != =~ !~ | equality; =~ and !~ compare strings ignoring case |
< <= > >= | ints, or strings in ordinal order |
+ - | ints only: join strings with interpolation or concat() |
* / % | ints; division truncates, dividing by zero or overflowing is an error |
!x -x | not, negation |
x.name x.?name x[i] x[?i] f(...) sys.f(...) | member, safe member, index, safe index, call |
A safe access (.?, [?) answers null where a missing property, an index out of range or a null would be an error, for the rest of the chain: settings.?tls.port is null when tls is missing. There are no decimal numbers; write json('1.5') for one.
Object keys are compared ignoring case, as ARM compares them: { Name: 'x' }.name is 'x', and an object cannot have both a and A.
A lambda, x => x * 2 or (acc, x) => acc + x, can only be passed to filter, map, sort, reduce and toObject.
param ports int[] = [
514
1514
]
var doubled = map(ports, p => p * 2)
var named = [for (p, i) in ports: 'listener-${i}-${p}']
var high = filter(ports, p => p > 1000)
var total = reduce(ports, 0, (sum, p) => sum + p)
var byName = toObject(ports, p => 'port${p}')
var safe = ports[?5] ?? 0
Functions
Functions work as their ARM counterparts do. They may be written with the sys. namespace.
| Kind | Functions |
|---|---|
| String | concat, format ({0} placeholders, {0:D3}-style formats), toLower, toUpper, trim, replace, split, join, substring, startsWith, endsWith, indexOf, lastIndexOf, padLeft, string, uniqueString, guid, base64, base64ToString, uri, uriComponent, uriComponentToString, newGuid and utcNow (parameter defaults only) |
| Number | int, min, max, range |
| Logic | bool, not, empty, contains, coalesce |
| Array and object | array, length, first, last, take, skip, union, intersection, items, toObject, filter, map, sort, reduce, flatten, objectKeys, json, createObject, any |
| Deployment | deployment() answers { name }; tenant() answers { id, name } |
Worth knowing:
startsWith,endsWith,indexOfandlastIndexOfignore case;containsdoes not for strings and arrays, but does for an object's keys.string(true)is'True', as in ARM; an object or array becomes compact JSON.uniqueString(...)answers a 13-character hash of its arguments andguid(...)a UUID derived from them: the same arguments always answer the same value.unionmerges objects deeply (a nested array is replaced, not merged) and joins arrays keeping each value once.itemsandobjectKeyslist keys in alphabetical order.loadTextContentand the other file functions are not available, as a template deploys without a file system: put the text in a'''multi-line string'''.- An unknown function is an error listing the close matches.
Resource Types
Every type's version is 2026-09-27. properties holds the console's request body for the resource, without its name.
| Type | name | properties | Notes |
|---|---|---|---|
VirtualMetric/directors | director name | mode, containerType, processingMode, stage (create only), status, properties (object: proxy_tls, serverless, autoscale, persistent_storage, ...), tokenEnabled, credentialIds | Creating one answers its API key and install script once, in the deployment's live response only. Changing mode, containerType, processingMode or stage later is refused. |
VirtualMetric/clusters | cluster name | directors ([{ id, ipAddress?, port? }], at least 3), port, properties (object), status, type | Needs the cluster feature. A member left without an address and port keeps its own, or takes its director's address and port 8081 when new. |
VirtualMetric/devices | device name | type, description, tags, pipelineId, relations ([{ id, type }], required), managerId, parentId, parentType, tokenId, properties (object), status, credentialIds | type cannot change. An agent can be referenced as existing (its lookup answers it read-only, marked managedBy: 'agent'), but agents are managed by the agent API: a template cannot create or update one. |
VirtualMetric/targets | target name | type, description, filter, pipelineId, properties (object), status, credentialIds, libraries, proxyOptions, relations, detectionRuleIds | The template is the target's whole state: a list it leaves out (relations, libraries, detection rules, credentials) is cleared. |
VirtualMetric/pipelines | pipeline name (a-z, 0-9, _) | title, description, longDescription, deviceFamily, deviceVendor, deviceType, content (YAML), stage ('main' or 'staging'), review (leave the commit pending instead of merging) | Content goes through a commit, as an edit in the console does. Each pipeline resource lands in a commit of its own: a pipeline and its children are separate commits, merged one after the other in dependency order, the parent first. The parent is therefore live before its new children are: if one of them then fails, the parent stays live without it until the cause is fixed and the template deployed again. With review: true nothing is merged and PIPELINE_COMMIT_MERGE is not needed; the deployment then never rejects a stale commit: it leaves your own pending commits, with a warning, and refuses up front when a stale commit still reserves the name of a child it would create. |
VirtualMetric/pipelines/children | child name | content, description, deviceType, title, longDescription, deviceFamily, deviceVendor | Child of a pipeline, in a commit of its own after its parent's (see above). A property the template leaves out is cleared, and the what-if shows it. |
VirtualMetric/pipelines/stagings | 'staging' | {} | Child of a pipeline: makes sure its staging clone exists. |
VirtualMetric/pipelines/stagings/syncs | 'sync' | endStaging (delete the clone after syncing, so the main pipeline is editable again) | An action, child of a staging: it runs on every deployment. Needs PIPELINE_DELETE only with endStaging: true; then it first checks that the clone can be deleted (the GitOps lock, whether it is in use or invoked by another pipeline) and refuses before anything goes live. |
VirtualMetric/quickRoutes | '<device name>/<target name>' | deviceId, targetId, pipelineId, configuration (object or YAML) | Found by its device and target names, so a what-if tells create from modify: name routes '<device>/<target>' to update them in place. A name without / is only a label and creates a route: when the edge already exists it is refused before anything is written, asking for the '<device name>/<target name>' name. Puts the device and the target on the canvas when missing. Needs routes.quick. |
VirtualMetric/advancedRoutes | route name | status, description, filter, pipelineId, defaultTargetId, properties (object), relations | Replaced as a whole. Needs routes.advanced. |
VirtualMetric/contentHub/packs | pack key | optionalPipelines (pack keys) | Installs the pack, or adds the optional packs it is missing. Needs content_hub.access. |
VirtualMetric/contentHub/routes | route name | pack, status, description, filter, defaultTargetId, properties, relations | Installs a pack's route, then keeps it up to date. |
VirtualMetric/library/grokPatterns | name | pattern, description | |
VirtualMetric/library/schemas | name | version, description, schemaFormat, schemaText | |
VirtualMetric/library/lookups | name | description, storageType, format, text | |
VirtualMetric/library/scripts | name | description, language, content | |
VirtualMetric/library/samples | name | description, vendor, format, kind, useCases, deviceTypes, definitionId, pipelineIds, template, placeholders | |
VirtualMetric/library/snmpVendors | sysObjectID (.1.3.6...) | vendor, deviceClass, model, hidden | |
VirtualMetric/datasets | dataset name | the public API's body | A device is in datasets or in profiles, never both: assigning a device to a dataset removes it from its profiles, and the deployment warns about each one it leaves. |
VirtualMetric/profiles | profile name | the public API's body | Assigning a device to a profile removes it from its datasets, with a warning for each. |
VirtualMetric/vault/secrets | secret name | the console's body, the secret value from a @secure() parameter (VM042 otherwise) | status is accepted as 'active' only. providerId must name a provider of the secret's type in the tenant. |
The console's Deploy page, and GET /api/v1/deployments/resource-types, list the types the backend serves with each property, its type and whether it is required.
Examples
A Syslog Device
A UDP syslog listener on port 514 on an existing director. The director is found by name, so the template deploys to any tenant with a director of that name; the form offers a picker of your directors for directorName.
metadata description = 'A syslog listener on a director'
@description('Name of the device.')
param deviceName string = 'syslog-udp'
@description('The director the device listens on.')
@metadata({ picker: 'director' })
param directorName string = 'director-01'
@allowed([
'udp'
'tcp'
])
param protocol string = 'udp'
@minValue(1)
@maxValue(65535)
param port int = 514
resource director 'VirtualMetric/directors@2026-09-27' existing = {
name: directorName
}
resource syslog 'VirtualMetric/devices@2026-09-27' = {
name: deviceName
properties: {
type: 'syslog'
description: 'Syslog on ${protocol}/${port}'
status: 'active'
relations: [
{
id: director.id
type: 'director'
}
]
properties: {
protocol: protocol
address: '0.0.0.0'
port: port
}
}
}
output deviceId string = syslog.id
Deploy it once and the device is created. Deploy it again and nothing changes. Deploy it with protocol tcp and port 1514 and the same device is updated in place; the what-if shows exactly those two properties changing.
A Device, a Target and a Quick Route
A firewall's syslog, through an existing pipeline, into Azure Blob Storage. The client secret is a @secure() parameter: supplied when you deploy, never stored with the deployment.
@metadata({ picker: 'director' })
param directorName string = 'director-01'
@metadata({ picker: 'pipeline' })
param pipelineName string = 'fw_syslog'
param storageAccount string
param tenantId string
param clientId string
@secure()
param clientSecret string
resource director 'VirtualMetric/directors@2026-09-27' existing = {
name: directorName
}
resource pipeline 'VirtualMetric/pipelines@2026-09-27' existing = {
name: pipelineName
}
resource firewall 'VirtualMetric/devices@2026-09-27' = {
name: 'fortigate-syslog'
properties: {
type: 'syslog'
status: 'active'
relations: [
{
id: director.id
type: 'director'
}
]
properties: {
protocol: 'udp'
address: '0.0.0.0'
port: 514
}
}
}
resource archive 'VirtualMetric/targets@2026-09-27' = {
name: 'blob-archive'
properties: {
type: 'azblob'
status: 'active'
properties: {
tenant_id: tenantId
client_id: clientId
client_secret: clientSecret
account: storageAccount
container: 'vmetric'
name: 'vmetric.{{.Timestamp}}.{{.Extension}}'
format: 'json'
}
}
}
resource route 'VirtualMetric/quickRoutes@2026-09-27' = {
name: 'fortigate-syslog/blob-archive'
properties: {
deviceId: firewall.id
targetId: archive.id
pipelineId: pipeline.id
}
}
The route refers to the device and the target, so they deploy first; the route puts both on the canvas if they are not there yet. Its name is '<device name>/<target name>', which is how a deployment finds the route again and updates it in place; a name without / is only a label, which can create a route but is refused when the device and the target are already connected.
A Pipeline Change Through Staging
Staging lets a pipeline change be tried on its staging clone before it reaches the main pipeline. This template makes sure the clone exists, writes the new content to it (stage: 'staging'), then syncs it onto the main pipeline and ends staging, so the main pipeline is editable again. The sync is an action: it runs on every deployment of the template.
resource pipeline 'VirtualMetric/pipelines@2026-09-27' existing = {
name: 'fw_syslog'
}
resource staging 'VirtualMetric/pipelines/stagings@2026-09-27' = {
parent: pipeline
name: 'staging'
properties: {}
}
resource stagingContent 'VirtualMetric/pipelines@2026-09-27' = {
name: 'fw_syslog'
properties: {
stage: 'staging'
description: 'Parse firewall syslog'
content: '''
name: fw_syslog
description: Parse firewall syslog
processors:
- syslog:
field: message
- set:
field: observer.vendor
value: "Fortinet"
'''
}
dependsOn: [
staging
]
}
resource sync 'VirtualMetric/pipelines/stagings/syncs@2026-09-27' = {
parent: staging
name: 'sync'
properties: {
endStaging: true
}
dependsOn: [
stagingContent
]
}
A pipeline's content is its YAML, written verbatim between triple quotes; its name: is the pipeline's own. Set review: true on the pipeline to leave the commit pending for review instead of merging it. Every pipeline resource is a commit of its own: a pipeline and its children are not one commit, but one each, merged in dependency order with the parent first.
A Serverless Director with Persistent Storage
A serverless director on Azure Container Apps that keeps what it is sending on disk for two days, publishes port 1514, and scales to two replicas.
param directorName string = 'serverless-01'
resource director 'VirtualMetric/directors@2026-09-27' = {
name: directorName
properties: {
mode: 'serverless'
containerType: 'azure-container-apps'
processingMode: 'persistent'
properties: {
proxy_tls: {
status: true
mode: 'self-signed'
port: '8443'
discovery_enabled: false
}
persistent_storage: {
max_age: 172800
}
serverless: {
exposed_ports: {
'1514': '21514'
}
}
autoscale: {
max_replicas: 2
}
}
}
}
output directorId string = director.id
Creating a director answers its API key and install script once: they are in the deployment's live response (the operation's outputs) for 15 minutes, to whoever started the deployment, and are not shown again. Its mode, containerType, processingMode and stage cannot change after it is created: a template that changes one is refused before anything is written, and its what-if says so (VM043).
Exporting Templates
Any device, agent, target, advanced route, pipeline, director, cluster, dataset or profile can be exported from its detail page (POST /api/v1/deployments/export. The exported template:
- declares a parameter for the resource's name, defaulted to its current name;
- turns every id it holds into an
existingreference, with its own name parameter (a picker in the form), so it deploys to another tenant where the ids differ; ids of a resource type you may not read stay as they are, and the export says so, and so does an id whose resource cannot be found by name again (a vault secret or a cluster whose name another one shares), which stays a literal id; - declares each secret as an
@secure()parameter whose empty default keeps the stored value; - outputs the resource's id.
A vault secret's providerId is the id of a provider in the tenant it was exported from, and the export warns so: deployed to another tenant, it must name a provider of the right type there.
Deployed back unchanged, an exported template changes nothing. Change a value and deploy it, and the resource is updated in place.
The create wizards offer the same through @secure() parameter to supply when you deploy.
Diagnostics and Limits
Every finding has a code, a message, a severity and the range it applies to, so the editor can underline it. A code BCP### means what it means in Bicep; VM### codes are VirtualMetric's. A syntax error is reported once: the rest of the template is still read and checked.
Syntax and Names
| Code | Meaning |
|---|---|
BCP001 | a character that starts no token |
BCP002 | a /* comment that is never closed |
BCP004, BCP005 | a string not closed before the end of its line, or of the template |
BCP006 | an escape a string does not know |
BCP007 | not a declaration (or a nested resource outside a resource's body) |
BCP009 | an expression was expected |
BCP010 | an integer outside the 64-bit range |
BCP013 | a name was expected |
BCP018 | a character was expected here (=, ), } and so on) |
BCP019 | a new line was expected |
BCP020 | a property or function name was expected after . |
BCP022 | a property name was expected in an object |
BCP025 | a property declared twice in an object (keys compare ignoring case) |
BCP028 | a name declared twice, or a reserved one (sys, az) |
BCP057 | a name that does not exist here, with the close matches |
BCP059 | a call to a name that is not a function |
BCP079 | a declaration that refers to itself |
BCP080 | declarations, or resources, that refer to each other in a cycle; the message shows it |
BCP082 | a function that does not exist, with the close matches |
BCP107 | a function of a namespace other than sys |
BCP140 | a ''' string that is never closed |
BCP242 | a lambda anywhere but as a function's argument |
Types, Values and Decorators
| Code | Meaning |
|---|---|
BCP027 | a parameter's default that does not fit its type |
BCP032 | a value that must be a constant (a decorator's argument, metadata) refers to a name or to deployment() or tenant() |
BCP033 | a typed variable's or an output's value that does not fit its type |
BCP047 | interpolation in a resource type or in a type |
BCP065 | newGuid() or utcNow() outside a parameter's default |
BCP070 | an argument of the wrong kind: a lambda where none belongs, something else where one does, a decorator argument of the wrong type |
BCP071 | the wrong number of arguments |
BCP072 | a parameter's default that refers to anything but other parameters, or reads deployment() |
BCP124 | a decorator on a value of the wrong type (@minValue on a string) |
BCP125, BCP126, BCP127, BCP129 | a decorator on the wrong kind of declaration (parameter, variable, resource, output; BCP125 for the rest) |
BCP130 | decorators where none are allowed |
BCP132 | a decorator not followed by a declaration |
BCP302 | a type that does not exist, or a union that is not of literals of one kind |
VM006 | a decorator given twice |
VM009 | a decimal number: write json('1.5') |
Resources
| Code | Meaning |
|---|---|
BCP029 | a resource type that is not '<Type>@<version>', or a resource-typed parameter or output |
BCP035 | a resource without a name |
BCP036 | name, properties, parent or dependsOn of the wrong kind |
BCP037 | a property a resource's body does not take: only name, properties, parent and dependsOn |
BCP073 | properties on an existing resource |
BCP144 | a resource loop read without an index |
BCP156 | a nested resource's type that is not one segment |
BCP160 | a resource nested in a resource loop |
BCP171 | a child type that is not a child of its parent's type |
BCP178 | a resource loop over a value known only as the deployment runs |
VM007 | parent: on a resource already nested in one |
VM020, VM021 | an unknown resource type or version; the message lists the known ones |
VM022 | a child without the right parent |
VM023 | a parent on a type that takes none |
VM024 | an existing action |
VM025 | two resources that would deploy the same resource (type, parent and name); both are named |
VM040 | a resource's properties refused by its type |
VM042 | a secret written into the template or passed through a plain parameter: it must come through a @secure() parameter |
VM043 | a change an existing resource cannot take, such as a director's mode or a device's type (what-if only: the deployment refuses it before its first write) |
What Templates Do Not Support
| Code | Meaning |
|---|---|
VM001 | func declarations |
VM002 | modules |
VM003 | import, using, extension, provider, test, assert, @export() |
VM004 | a target scope other than 'tenant' |
VM005 | a file function (loadTextContent and the like) |
VM008 | the spread operator ... |
Parameters, Evaluation and Limits
| Code | Meaning |
|---|---|
VM010 | the template is larger than 1 MiB |
VM011 | the template deploys more than 800 resources |
VM013 | nested too deeply: expressions, types or resources nested more than 128 levels as written; any expression more than 1,000 levels deep, where each operand of an operator chain and each property access counts one; a declared type resolved or required through more than 64 others |
VM014 | a constant (a decorator's argument, metadata, a default that refers to nothing) that cannot be evaluated |
VM015 | the language itself failed on the template: an internal error, reported instead of failing the request. The template may be fine; report it |
VM030 | a parameter without a value or a default |
VM031 | a parameter's value or default that is refused: its type, @allowed, a range or length, *** as a secure value, a value too large or nested too deep, a default known only as the deployment runs |
VM032 | a parameter the template does not declare (the first 20 are named) |
VM041 | an expression that fails before the deployment starts |
VM403 | a permission or feature you lack; one finding per missing one |
VM500 | the permission check could not be completed; the API answers 500 UNKNOWN_ERROR for it |
Limits
- A template is at most 1 MiB and deploys at most 800 resources; a loop has at most 800 iterations, and
range()makes at most 10,000 numbers. A deployment runs for at most 30 minutes. - A string is at most 4 MiB. A value a template hands on (a resource's properties, an output, a parameter,
string()of an object) has at most 262,144 items once repeated parts are counted each time, and nests at most 128 levels. - Evaluating a template has a work budget: each expression, each item a function makes and the text a function reads count against it. A template past it is refused with "too much work" under the code of where it happened (
VM014for a constant,VM031for a default,VM041for a resource's expression) or, as the deployment runs, fails its operation withDEPLOYMENT_EXPRESSION_FAILED. Evaluation nests at most 256 levels. - Checking a value against its type or its
@allowedlist stops after 262,144 comparisons, and says the value is too large to check. Close matches ("Did you mean") are searched for the first 25 unknown names.
Operation Errors
An operation that fails records an error code of its own, which is also the deployment's:
| Code | Meaning |
|---|---|
| the type's own code | a resource type refused the write, such as DEVICE_PORT_ALREADY_EXISTS |
RESOURCE_NOT_FOUND | an existing resource was not found |
DEPLOYMENT_OPERATION_FAILED | a failure without a code of its own, such as a storage error (recorded as <operation> failed: a storage error occurred) |
DEPLOYMENT_EXPRESSION_FAILED | an expression failed as the deployment ran |
DEPLOYMENT_TEMPLATE_INVALID | the type refused the evaluated properties, a secret did not come through a @secure() parameter, two resources turned out to be the same one, or a change a resource cannot take was refused before the first write (VM043 in the what-if) |
DEPLOYMENT_OUTPUT_FAILED | an output could not be evaluated |
DEPLOYMENT_CANCELED | a cancel stopped the deployment before this point |
DEPLOYMENT_TIMED_OUT | the deployment ran past its 30 minutes |
DEPLOYMENT_INTERRUPTED | the server running it stopped reporting progress (the operation may or may not have been applied) |
DEPLOYMENT_OPERATION_UNFINISHED | the deployment ended without this operation reporting its end |
DEPLOYMENT_INTERNAL_ERROR | the deployment stopped on an internal error |