One JavaScript user-defined aggregate on a Stream Analytics job β a routine the job's query calls over a window of rows β targeting
hashicorp/azurerm ~> 4.0.
- πͺ Declares one JavaScript aggregate the job's query can call over a window rather than a row.
- π Reports whether the script declares
init,accumulateandcomputeResultβ the shape that makes it an aggregate at all, and which nothing else checks. - π€ Emits the aggregate's signature in order β the order is the positional argument contract.
- πͺ Names the two ways this resource differs from its scalar twin: this one takes the job as a single
Resource ID, and validates
namefar more strictly. - π Emits the script's length and hash, never the body β the provider does not mark it sensitive.
- π·οΈ Carries no
tagsand nolocation; the universal tail istimeoutsonly.
π‘ Why it matters: an aggregate written like a scalar function β a plain
function name(args) { ... }β applies perfectly cleanly and aggregates nothing. Stream Analytics expects an object with three named members, and no check anywhere in Terraform or Azure looks for them before the job runs. This module looks, and reports.
If this module saved you time:
- β Star the repository β it helps other people find it.
- πΌ Connect on LinkedIn β linkedin.com/in/microsoftexpert
- β Buy me a coffee β buymeacoffee.com/microsoftexpert
flowchart TB
RG["terraform-azurerm-resource-group"]
JOB["terraform-azurerm-stream-analytics-job"]
THIS["terraform-azurerm-stream-analytics-function-javascript-uda"]
UDF["terraform-azurerm-stream-analytics-function-javascript-udf"]
QUERY["the job's transformation query, which calls it OVER A WINDOW"]
RG -->|"name"| JOB
JOB -->|"id, a single Resource ID"| THIS
JOB -->|"name, plus the resource group separately"| UDF
THIS -.->|"parses out the job name and resource group the function wants"| UDF
THIS -.->|"is called from, but does not manage"| QUERY
classDef me fill:#0078D4,stroke:#004578,stroke-width:2px,color:#ffffff
classDef target fill:#004578,stroke:#00243d,stroke-width:2px,color:#ffffff
classDef sib fill:#eef3f8,stroke:#9db4c9,color:#1a2733
class THIS me
class JOB target
class RG,UDF,QUERY sib
The two dotted edges are the point. This module parses out the job name and resource group that the scalar function resource takes as arguments β and the transformation query, which is where the aggregate is actually called from, lives on the job and is not managed here at all.
flowchart TB
NAME["name, 3 to 63 letters digits hyphens -- stricter than the function"]
JOBID["stream_analytics_job_id, one Resource ID"]
IN["input, an ORDERED list, at least one"]
OUT["output, one value PER WINDOW"]
SCRIPT["script, needs init accumulate computeResult"]
THIS["azurerm_stream_analytics_function_javascript_uda.this"]
OID["id"]
OSIG["input_types in order, output_type"]
OPARSED["job name and resource group, what the function twin wants"]
OMEM["declares_all_three_aggregate_members, reported not enforced"]
NAME --> THIS
JOBID --> THIS
IN --> THIS
OUT --> THIS
SCRIPT --> THIS
THIS --> OID
THIS --> OSIG
THIS --> OPARSED
THIS --> OMEM
classDef me fill:#0078D4,stroke:#004578,stroke-width:2px,color:#ffffff
classDef target fill:#004578,stroke:#00243d,stroke-width:2px,color:#ffffff
classDef sib fill:#eef3f8,stroke:#9db4c9,color:#1a2733
class THIS me
class OID target
class NAME,JOBID,IN,OUT,SCRIPT,OSIG,OPARSED,OMEM sib
Resource inventory
| Resource | Count | Notes |
|---|---|---|
azurerm_stream_analytics_function_javascript_uda |
1 (this) |
one aggregate on one job |
/subscriptions/SUB/resourceGroups/RG/providers/Microsoft.StreamAnalytics/streamingJobs/JOB/functions/NAME
The same ID shape the scalar function produces β from a different set of arguments.
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module β the caller configures the provider, its authentication, and the mandatory features {} block |
| Resources created | 1 |
Schema notes that bite
- π΄
nameenforces^[a-zA-Z0-9-]{3,63}$here, while the scalar function twin checks only that the name is non-empty.my_uda,abandmy.udaare all accepted there and refused here β verified by comparing both validators. - π΄ The job is taken as a single Resource ID, while the scalar function takes a name plus a resource
group. Both compose the same Function Resource ID; neither accepts the other's shape, and this resource has
no
resource_group_nameargument at all. - π΄ An aggregate's script must expose
init,accumulateandcomputeResultβ called once per window, once per row, and once at the end. Nothing checks for them. A script shaped like a plain function body, which is correct for the scalar twin, applies cleanly and aggregates nothing. - π΄ Nothing compiles, parses or runs the script.
- π΄ The script is not marked sensitive, so anything embedded in it is in plan output and in state in clear.
β οΈ inputrequires at least one block, and the provider reports its absence as a bare minimum-items error.β οΈ Input order is the positional argument contract. Reordering the list silently rebinds the query's arguments.β οΈ The provider's own name-validation message prints the VALUE where a field name belongs β it reads "<your value> contain only letters, numbers and hyphens" β so this module supplies its own wording.β οΈ The script has no difference suppressor, so reindenting the body produces a diff and no service-side change.- The seven types are
any,array,bigint,datetime,float,nvarchar(max),recordβ the same seven the scalar function accepts; the two lists differ only in the order the provider's source writes them. - Force-new:
nameandstream_analytics_job_id. Note that is two arguments where the scalar twin has three, because that resource splits the job reference.
Least-privilege, at the smallest scope that works:
Microsoft.StreamAnalytics/streamingJobs/functions/write,.../readand.../deleteon the target job. A custom role scoped to the job is enough; Contributor on the job, or on its resource group, covers this more broadly than necessary.
β οΈ Write access here is the ability to change what the job computes, over whole windows of data. The script runs inside the streaming pipeline, and a plan shows a string.
βΉοΈ Plan access is not credential access β provided the script carries no credential. The provider does not mark it sensitive.
- An existing Stream Analytics job.
- A transformation query on that job that calls this aggregate over a window, or it computes nothing. That query lives on the job resource and is not managed by this module.
- The caller configures
provider "azurerm" { features {} }, authentication and subscription.
terraform-azurerm-stream-analytics-function-javascript-uda/
βββ providers.tf # required_version + pinned azurerm; no provider block
βββ variables.tf # 6 inputs, 11 validations, the twin's differences documented inline
βββ main.tf # the single keystone `this` + a dynamic input block, output rendered statically
βββ outputs.tf # 31 outputs; the signature, the three members, the hash but never the body
βββ README.md # this file
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT
βββ .gitignore
provider "azurerm" {
features {}
}
module "uda_weighted_avg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-stream-analytics-function-javascript-uda.git?ref=v1.0.0"
name = "uda-weighted-avg"
stream_analytics_job_id = module.job.id
input = [{ type = "float" }]
output = { type = "float" }
script = <<-JS
function main() {
this.init = function () {
this.total = 0.0;
this.count = 0;
}
this.accumulate = function (value, timestamp) {
this.total += value;
this.count += 1;
}
this.computeResult = function () {
return this.count === 0 ? 0.0 : this.total / this.count;
}
}
JS
}
β οΈ Note the name: hyphens, not underscores.uda_weighted_avgis rejected here and would be accepted on the scalar function resource.
βΉοΈ The caller configures the provider, its authentication, and the mandatory
features {}block. This module declares none of them.
Consumes
| Input | Type | Source |
|---|---|---|
stream_analytics_job_id |
string |
terraform-azurerm-stream-analytics-job output id |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Resource ID of the aggregate | review |
name |
Name the job's query must call | the transformation query |
stream_analytics_job_id |
The job it belongs to | sibling modules |
stream_analytics_job_name |
Job name, parsed from that ID | the scalar twin |
resource_group_name |
Resource group, parsed from that ID | the scalar twin |
input_types |
Declared parameter types, in order | signature review |
input_count |
How many parameters | signature review |
configuration_parameter_positions |
Which parameters are per-window constants | design review |
accumulates_no_per_row_value |
True when nothing varies per row | design review |
output_type |
The single return type per window | signature review |
returns_a_structure |
True for record or array |
query review |
declares_init |
Script appears to declare init |
script review |
declares_accumulate |
Script appears to declare accumulate |
script review |
declares_compute_result |
Script appears to declare computeResult |
script review |
declares_all_three_aggregate_members |
All three appear | script review |
script_length |
Characters in the script | change review |
script_sha256 |
Fingerprint of the script | change review |
the_name_rule_is_stricter_here_than_on_the_scalar_function |
Constant true | naming standards |
the_scalar_twin_names_the_job_by_name_instead |
Constant true | design review |
an_aggregate_expects_an_object_not_a_function_body |
Constant true | script review |
nothing_compiles_or_runs_the_script |
Constant true | operational review |
the_declared_types_are_a_contract_with_the_query_not_the_script |
Constant true | design rationale |
input_order_is_the_argument_contract |
Constant true | change planning |
script_formatting_changes_produce_a_diff |
Constant true | drift review |
the_script_is_not_marked_sensitive |
Constant true | security review |
force_new_fields |
The name and the job ID | change planning |
fields_azure_returns_on_read |
Where drift is detectable | drift review |
create_refuses_an_existing_aggregate |
Constant true | import review |
the_import_guard_can_be_disabled_by_a_provider_feature |
Constant true | design rationale |
delete_tolerates_an_aggregate_that_is_already_gone |
Constant true | destroy review |
this_resource_supports_no_azure_resource_tags |
Constant true | tagging policy |
1 Β· A running average over a window
module "uda_average" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-stream-analytics-function-javascript-uda.git?ref=v1.0.0"
name = "uda-average"
stream_analytics_job_id = module.job.id
input = [{ type = "float" }]
output = { type = "float" }
script = <<-JS
function main() {
this.init = function () {
this.total = 0.0;
this.count = 0;
}
this.accumulate = function (value, timestamp) {
this.total += value;
this.count += 1;
}
this.computeResult = function () {
return this.count === 0 ? 0.0 : this.total / this.count;
}
}
JS
}π‘
initruns once per window,accumulateonce per row,computeResultonce at the end. That three-part shape is the whole difference between an aggregate and a scalar function.
2 Β· π΄ The mistake that applies cleanly and aggregates nothing
module "uda_broken" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-stream-analytics-function-javascript-uda.git?ref=v1.0.0"
name = "uda-broken"
stream_analytics_job_id = module.job.id
input = [{ type = "float" }]
output = { type = "float" }
# This is a SCALAR function body. It is correct for the udf resource and
# wrong here -- and nothing at any stage says so.
script = "function average(value) { return value; }"
}
output "is_it_actually_an_aggregate" {
value = module.uda_broken.declares_all_three_aggregate_members # false
}π΄
an_aggregate_expects_an_object_not_a_function_bodyistrue, andnothing_compiles_or_runs_the_scriptis too. This applies successfully and reads back cleanly. Thedeclares_*outputs are the only warning anything gives β and they are text searches, described as such, not a compiler.
3 Β· Configuration parameters β constants for the whole window
module "uda_weighted_avg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-stream-analytics-function-javascript-uda.git?ref=v1.0.0"
name = "uda-weighted-avg"
stream_analytics_job_id = module.job.id
input = [
{ type = "float" }, # per row
{ type = "bigint", configuration_parameter = true }, # once per window
]
output = { type = "float" }
script = <<-JS
function main() {
this.init = function () { this.total = 0.0; this.weight = 0; }
this.accumulate = function (value, weight) {
this.total += value * weight;
this.weight += weight;
}
this.computeResult = function () {
return this.weight === 0 ? 0.0 : this.total / this.weight;
}
}
JS
}
β οΈ If every parameter is a configuration parameter,accumulates_no_per_row_valueistrueβ which on an aggregate specifically is almost always wrong: it computes the same answer for every window.
4 Β· Returning several statistics at once
module "uda_stats" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-stream-analytics-function-javascript-uda.git?ref=v1.0.0"
name = "uda-stats"
stream_analytics_job_id = module.job.id
input = [{ type = "float" }]
output = { type = "record" }
script = <<-JS
function main() {
this.init = function () { this.n = 0; this.sum = 0.0; this.min = null; this.max = null; }
this.accumulate = function (v, t) {
this.n += 1;
this.sum += v;
if (this.min === null || v < this.min) { this.min = v; }
if (this.max === null || v > this.max) { this.max = v; }
}
this.computeResult = function () {
return { n: this.n, mean: this.n ? this.sum / this.n : 0.0, min: this.min, max: this.max };
}
}
JS
}π‘
returns_a_structureistrue. An aggregate returning a record is the efficient way to compute several statistics in one pass β but the query then has to unpack it, and nothing here can tell you whether it does.
5 Β· The name rule, and what it rejects
module "uda_weighted_avg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-stream-analytics-function-javascript-uda.git?ref=v1.0.0"
name = "uda_weighted_avg" # <-- rejected
stream_analytics_job_id = module.job.id
input = [{ type = "float" }]
output = { type = "float" }
script = "function main() { this.init = function () {}; this.accumulate = function () {}; this.computeResult = function () { return 0; } }"
}Error: Invalid value for variable
name contains an underscore, which this resource does not permit -- although
the SCALAR function resource does. Use a hyphen instead: "weighted_average"
becomes "weighted-average".
π‘ The provider's own message for this reads "<your value> contain only letters, numbers and hyphens" β it prints the value where a field name belongs and reads ungrammatically β so this module supplies its own.
6 Β· πͺ The difference from the scalar twin
# THIS resource: one job ID, and a strict name rule.
module "uda_weighted_avg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-stream-analytics-function-javascript-uda.git?ref=v1.0.0"
name = "uda-weighted-avg"
stream_analytics_job_id = module.job.id
input = [{ type = "float" }]
output = { type = "float" }
script = "function main() { this.init = function () {}; this.accumulate = function (v, t) {}; this.computeResult = function () { return 0; } }"
}
# The SCALAR function: a job NAME and a resource group, and a loose name rule.
# Both are available from THIS module's outputs.
module "udf_to_celsius" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-stream-analytics-function-javascript-udf.git?ref=v1.0.0"
name = "udf_to_celsius" # underscores are FINE there
stream_analytics_job_name = module.uda_weighted_avg.stream_analytics_job_name
resource_group_name = module.uda_weighted_avg.resource_group_name
input = [{ type = "float" }]
output = { type = "float" }
script = "function toCelsius(f) { return (f - 32) * 5 / 9; }"
}π΄
the_name_rule_is_stricter_here_than_on_the_scalar_functionandthe_scalar_twin_names_the_job_by_name_insteadare bothtrue. Thestream_analytics_job_nameandresource_group_nameoutputs exist precisely so the scalar function can be wired from here rather than from two separate places.
π‘ Adopt this module's stricter naming rule for both kinds and you will have one convention rather than two.
7 Β· Several aggregates with `for_each`
locals {
udas = {
"uda-average" = {
input = [{ type = "float" }]
output = "float"
script = file("$${path.module}/scripts/average.js")
}
"uda-p95" = {
input = [{ type = "float" }]
output = "float"
script = file("$${path.module}/scripts/p95.js")
}
}
}
module "udas" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-stream-analytics-function-javascript-uda.git?ref=v1.0.0"
for_each = local.udas
name = each.key
stream_analytics_job_id = module.job.id
input = each.value.input
output = { type = each.value.output }
script = each.value.script
}π‘ Key the map by the aggregate name β it is force-new and the identifier the query calls, so a stable key keeps adding one from re-creating the rest.
β οΈ Keeping the JavaScript in real.jsfiles means an editor and a linter can see it, which is worth something given that nothing else ever will.
8 Β· Checking every aggregate looks like one
locals {
malformed = [
for k, m in module.udas : k if !m.declares_all_three_aggregate_members
]
degenerate = [
for k, m in module.udas : k if m.accumulates_no_per_row_value
]
}
check "aggregates_look_like_aggregates" {
assert {
condition = length(local.malformed) == 0
error_message = "These Stream Analytics aggregates do not appear to declare init, accumulate and computeResult: ${join(", ", local.malformed)}. Nothing in Terraform or Azure checks this before the job runs."
}
}
output "aggregate_review" {
value = {
signatures = { for k, m in module.udas : k => { args = m.input_types, returns = m.output_type } }
malformed = local.malformed
degenerate = local.degenerate
hashes = { for k, m in module.udas : k => m.script_sha256 }
}
}π‘ A
checkblock rather than a validation, deliberately: thedeclares_*outputs are text searches, and a valid aggregate can be written in ways they will not match. Acheckreports without blocking β and avalidation {}failure would blockterraform destroyas well as apply.
9 Β· Explicit timeouts
module "uda_average" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-stream-analytics-function-javascript-uda.git?ref=v1.0.0"
name = "uda-average"
stream_analytics_job_id = module.job.id
input = [{ type = "float" }]
output = { type = "float" }
script = file("$${path.module}/scripts/average.js")
timeouts = {
create = "30m"
read = "5m"
update = "30m"
delete = "30m"
}
}βΉοΈ The provider defaults are 30m / 5m / 30m / 30m. None of these operations compiles or runs the script, so none is long in practice β the defaults are headroom, not an expectation.
10 Β· Renaming, and what it breaks
# `name` is force-new AND the identifier the job's transformation query calls.
# Renaming is a clean replacement to Terraform and a broken reference to the
# query -- which lives on the job resource and is not managed here.
#
# name = "uda-weighted-avg-v2" # <-- the query still says "uda-weighted-avg"
#
# Create the new one alongside, update the query, then remove the old block.
β οΈ Terraform cannot see the query, so it cannot warn you.force_new_fieldshere has two members where the scalar twin's has three, because that resource splits the job reference across two arguments.
11 Β· A bare job name is rejected
module "uda_average" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-stream-analytics-function-javascript-uda.git?ref=v1.0.0"
name = "uda-average"
stream_analytics_job_id = "asa-telemetry" # <-- the shape the SCALAR twin wants
input = [{ type = "float" }]
output = { type = "float" }
script = "function main() { this.init = function () {}; this.accumulate = function () {}; this.computeResult = function () { return 0; } }"
}Error: Invalid value for variable
stream_analytics_job_id does not look like an Azure Resource ID at all -- it
must begin "/subscriptions/". A bare job name belongs on the SCALAR function
resource, which takes stream_analytics_job_name and resource_group_name
instead; this one takes the job's `id` output.
π The pattern is anchored at both ends, so this module's own function ID β which extends the same path with
/functions/NAMEβ is rejected too, with its own message.
12 Β· Reviewing the script without emitting it
output "aggregate_scripts" {
value = {
for k, m in module.udas : k => {
size = m.script_length
fingerprint = m.script_sha256
init = m.declares_init
accumulate = m.declares_accumulate
compute = m.declares_compute_result
}
}
}π
the_script_is_not_marked_sensitiveistrue. The body is deliberately never emitted: re-emitting it would copy anything embedded in it into every consuming configuration's state as well as this one's.
β οΈ The fingerprint moves when the formatting does βscript_formatting_changes_produce_a_diffistrueβ so compare hashes across environments, not across reformats.
13 Β· ποΈ End-to-end composition
Resource group + Stream Analytics job + an aggregate and a scalar function on it, both wired from this module's outputs.
provider "azurerm" {
features {}
}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-streaming-prod"
location = "eastus2"
}
module "job" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-stream-analytics-job.git?ref=v1.0.0"
name = "asa-telemetry"
resource_group_name = module.rg.name
location = module.rg.location
transformation_query = <<-SQL
SELECT
deviceId,
udf-to-celsius(temperatureF) AS temperatureC,
uda-weighted-avg(temperatureF, 2) AS weighted
INTO [output]
FROM [input] TIMESTAMP BY eventTime
GROUP BY deviceId, TumblingWindow(minute, 5)
SQL
tags = {
environment = "prod"
workload = "telemetry"
}
}
module "uda_weighted_avg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-stream-analytics-function-javascript-uda.git?ref=v1.0.0"
name = "uda-weighted-avg"
stream_analytics_job_id = module.job.id
input = [
{ type = "float" },
{ type = "bigint", configuration_parameter = true },
]
output = { type = "float" }
script = <<-JS
function main() {
this.init = function () {
this.total = 0.0;
this.weight = 0;
}
this.accumulate = function (value, weight) {
this.total += value * weight;
this.weight += weight;
}
this.computeResult = function () {
return this.weight === 0 ? 0.0 : this.total / this.weight;
}
}
JS
}
# The scalar function takes a NAME and a RESOURCE GROUP -- both parsed above.
module "udf_to_celsius" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-stream-analytics-function-javascript-udf.git?ref=v1.0.0"
name = "udf-to-celsius"
stream_analytics_job_name = module.uda_weighted_avg.stream_analytics_job_name
resource_group_name = module.uda_weighted_avg.resource_group_name
input = [{ type = "float" }]
output = { type = "float" }
script = <<-JS
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
JS
}
check "the_aggregate_looks_like_an_aggregate" {
assert {
condition = module.uda_weighted_avg.declares_all_three_aggregate_members
error_message = "uda-weighted-avg does not appear to declare init, accumulate and computeResult. Nothing in Terraform or Azure checks this before the job runs."
}
}
output "job_function_signatures" {
value = {
aggregate = {
name = module.uda_weighted_avg.name
arguments = module.uda_weighted_avg.input_types
returns = module.uda_weighted_avg.output_type
well_formed = module.uda_weighted_avg.declares_all_three_aggregate_members
hash = module.uda_weighted_avg.script_sha256
}
scalar = {
name = module.udf_to_celsius.name
arguments = module.udf_to_celsius.input_types
returns = module.udf_to_celsius.output_type
hash = module.udf_to_celsius.script_sha256
}
}
}π΄ The
transformation_queryon the job is what actually calls these two, and neither module manages it. If the names in the query and the names here drift apart, the job fails at run time and every plan stays clean.
π‘ Note the whole composition needs the job's name and resource group in exactly one place β here, parsed from the job ID this module already required. The scalar function's own module can reconstruct the ID in the other direction if you prefer to root the composition there instead.
βΉοΈ Output names shown for sibling modules are the ones those modules emit at
v1.0.0; align them with the versions you consume.
| Input | Type | Required | Default |
|---|---|---|---|
name |
string |
β | β |
stream_analytics_job_id |
string |
β | β |
input |
list(object({ type, configuration_parameter })) |
β | β (at least one) |
output |
object({ type }) |
β | β |
script |
string |
β | β |
timeouts |
object({ create, read, update, delete }) |
β | null |
Full schemas
variable "name" {
type = string
# ^[a-zA-Z0-9-]{3,63}$ -- mirrors the provider exactly. The SCALAR function
# twin accepts anything non-empty, so a name may be legal there and not here.
}
variable "stream_analytics_job_id" {
type = string
# A full JOB Resource ID, anchored at both ends. The scalar twin takes a bare
# name plus a resource group instead; this resource has NO resource_group_name
# argument and derives the value.
}
variable "input" {
type = list(object({
type = string
configuration_parameter = optional(bool, false)
}))
# ORDER IS THE CONTRACT -- a list, not a set. At least one required.
# Types: any, array, bigint, datetime, float, nvarchar(max), record.
}
variable "output" {
type = object({
type = string
})
# An object, because the provider requires exactly one and caps it at one.
# One value per WINDOW, not per row.
}
variable "script" {
type = string
# Must expose init, accumulate and computeResult -- REPORTED, not enforced.
# Stored, never compiled. NOT marked sensitive by the provider.
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
# Provider defaults: create 30m, read 5m, update 30m, delete 30m.
}| Output | Description | Notes |
|---|---|---|
id |
Resource ID of the aggregate | Emitted first |
name |
Name the query must call | force-new |
stream_analytics_job_id |
The job it belongs to | |
stream_analytics_job_name |
Job name, parsed from that ID | what the scalar twin takes |
resource_group_name |
Resource group, parsed from that ID | what the scalar twin takes |
input_types |
Parameter types, in order | the signature |
input_count |
How many parameters | known at plan time |
configuration_parameter_positions |
Positions of the per-window constants | positions, not a count |
accumulates_no_per_row_value |
True when nothing varies per row | almost always a mistake |
output_type |
The return type per window | |
returns_a_structure |
True for record or array |
|
declares_init |
init appears in the script |
a text search |
declares_accumulate |
accumulate appears |
a text search |
declares_compute_result |
computeResult appears |
a text search |
declares_all_three_aggregate_members |
All three appear | reported, not enforced |
script_length |
Characters in the script | body never emitted |
script_sha256 |
Fingerprint of the script | changes on reformat |
the_name_rule_is_stricter_here_than_on_the_scalar_function |
Constant true |
|
the_scalar_twin_names_the_job_by_name_instead |
Constant true |
|
an_aggregate_expects_an_object_not_a_function_body |
Constant true |
|
nothing_compiles_or_runs_the_script |
Constant true |
|
the_declared_types_are_a_contract_with_the_query_not_the_script |
Constant true |
|
input_order_is_the_argument_contract |
Constant true |
|
script_formatting_changes_produce_a_diff |
Constant true |
|
the_script_is_not_marked_sensitive |
Constant true |
|
force_new_fields |
["name", "stream_analytics_job_id"] |
two, not three |
fields_azure_returns_on_read |
Where drift is detectable | the script genuinely is |
create_refuses_an_existing_aggregate |
Constant true |
|
the_import_guard_can_be_disabled_by_a_provider_feature |
Constant true |
the toggle is the caller's |
delete_tolerates_an_aggregate_that_is_already_gone |
Constant true |
|
this_resource_supports_no_azure_resource_tags |
Constant true |
tag the job instead |
No secret is emitted, and the script is deliberately not echoed back.
An aggregate is an object, and nothing checks that yours is one. Stream Analytics calls init once at
the start of each window, accumulate once per row, and computeResult once at the end. A script that is a
plain function name(args) { ... } β exactly the right shape for the scalar twin β applies cleanly here,
reads back cleanly, and aggregates nothing. There is no compiler, no schema check and no service-side
validation before the job runs. This module therefore searches for the three member names and reports
what it finds, in four outputs whose descriptions say plainly that a text search is what they are. It does
not refuse: a valid aggregate can be written in ways the search will not match, and a validation {} failure
would block terraform destroy as well as apply.
Two resources, one idea, two shapes. This resource takes stream_analytics_job_id; the scalar function
takes stream_analytics_job_name plus resource_group_name. This one enforces ^[a-zA-Z0-9-]{3,63}$ on the
name; the scalar function accepts anything non-empty β so my_uda and ab are legal there and refused here,
verified by comparing both validators rather than inferred from one. Both compose the same Function Resource
ID underneath. This module emits stream_analytics_job_name and resource_group_name, parsed from the job
ID it already required, so a configuration managing both kinds can wire them from one place.
Different parsing from the twin, not a copy of it. This module parses the job ID it was given β nine elements β while the scalar module parses the function ID it produces β eleven. The two expressions look similar and index different things; both index sets were confirmed against real IDs before either was written.
The signature is positional, so input is a list. Stream Analytics binds a query's arguments by
position. Reordering the list is a silent rebinding, which is why the module keeps it a list rather than a
set and emits input_types in order rather than sorted. On an aggregate the per-row versus per-window
distinction carries extra weight: accumulates_no_per_row_value reports the degenerate case where every
parameter is a constant, which computes the same answer for every window.
Names, not bodies. The provider does not mark script sensitive, so anything embedded in it is in plan
output and in state in clear. Re-emitting it would put it in every consuming configuration's state too, so
this module emits script_length, script_sha256 and the three member flags instead. Note that the hash
moves when the formatting does, because the provider suppresses no differences on this field.
Where each check actually fires. The name pattern, the job-ID anchor, the type enums and the
input/output shape rules are module validation {} blocks: they refuse a bad configuration at terraform plan, offline. The
provider's own name pattern, its non-empty check on the script and its minimum-items rule fire there too.
Whether the job exists is decided at apply; whether the script aggregates anything is decided when the job
runs.
The features {} dependence. The provider will not initialize without a caller-side
provider "azurerm" { features {} } block. That block also carries the toggle that can disable this
resource's create-time existence check.
| Concern | Default in this module | Opt-out |
|---|---|---|
| The three aggregate members | reported, never enforced β four outputs, described as text searches | assert them in a check block |
| Input order | preserved β a list, never a set | none; order is the contract |
Empty input |
rejected, with a message naming the reason | none |
output |
modelled as an object, since the provider requires exactly one | β |
| Type values | the provider's seven, mirrored | none |
| Name | the provider's pattern mirrored, plus an underscore-specific message | none |
| Script in outputs | never emitted; length, hash and member flags instead | β |
| Job reference confusion | a bare name is rejected, naming the scalar twin | β |
| Secrets | none accepted deliberately, none emitted | β |
| Tags | not supported by the resource | tag the job |
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module with ?ref=v1.0.0 β never a branch. This module is authored and verified plan-only; a
human applies from CI.
What validate and fmt cover, offline and without credentials
- All 11
validation {}blocks β two on the name, three on the job ID, two oninput, one onoutput, two onscriptand one ontimeoutsβ each proven to fire from a deliberately bad.tfvarsfile, and each proven not to fire from two fully-populated valid ones. - Every fixture was first checked to parse, because a malformed fixture reports no validation failure and reads exactly like a check that does not fire.
- The naming asymmetry is proven in both directions: a name with underscores fires here and is proven to pass on the scalar function's own fixtures.
- Every derived output expression, lifted into a console harness and driven in three states β a one-argument
aggregate with a full three-member script, a two-argument one with a configuration parameter returning a
record, and a degenerate case with only
initdeclared and every parameter a constant.
What only terraform plan or apply exercises
- Whether the job exists.
- Whether an aggregate of that name is already there, and whether the caller's
features {}block has disabled the check that would catch it.
What nothing in Terraform ever exercises
- Whether the script compiles, declares the three members in a form the service recognises, matches its declared signature, or returns the declared type. The failure appears when the job runs.
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-streaming-prod/providers/Microsoft.StreamAnalytics/streamingJobs/asa-telemetry/functions/uda-weighted-avg"
name = "uda-weighted-avg"
stream_analytics_job_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-streaming-prod/providers/Microsoft.StreamAnalytics/streamingJobs/asa-telemetry"
stream_analytics_job_name = "asa-telemetry"
resource_group_name = "rg-streaming-prod"
input_types = ["float", "bigint"]
input_count = 2
configuration_parameter_positions = [1]
accumulates_no_per_row_value = false
output_type = "float"
returns_a_structure = false
declares_init = true
declares_accumulate = true
declares_compute_result = true
declares_all_three_aggregate_members = true
script_length = 178
script_sha256 = "a020f6e7dacf0d6790bb57c5239a131c25139acafb3186a2c0478f2f1d937eff"
force_new_fields = ["name", "stream_analytics_job_id"]
| Symptom | Cause | Fix |
|---|---|---|
| The aggregate applied but returns nothing sensible | The script is a plain function body rather than an object with three members | Check declares_all_three_aggregate_members; rewrite as init / accumulate / computeResult |
Invalid value for variable ... contains an underscore |
This resource's name rule is stricter than the scalar function's | Use hyphens; adopt the stricter rule for both kinds |
Invalid value for variable ... does not look like an Azure Resource ID |
A bare job name was passed | This resource takes the job's id; the scalar twin takes the name |
| The same aggregate computes the same value for every window | Every parameter is a configuration parameter | Check accumulates_no_per_row_value; at least one input must vary per row |
| Reordering the input list changed behaviour | Arguments bind positionally | Restore the order; input_types shows it |
| The query cannot find the aggregate | The name in the query does not match name |
They must agree; the query lives on the job and is not managed here |
| A repository-wide reformat rewrote every aggregate | The script has no difference suppressor | Expected; reformat deliberately |
input must declare at least one parameter |
An empty list was passed | The provider requires one; an aggregate with no input has nothing to aggregate |
| Two aggregates collide on create | One of that name already exists | Import it, or pick another name; the guard can be disabled by a features {} toggle |
terraform destroy succeeded on an aggregate already deleted in the portal |
Delete tolerates a not-found response | Expected; a clean destroy is not proof this configuration performed it |
azurerm_stream_analytics_function_javascript_udaazurerm_stream_analytics_function_javascript_udfazurerm_stream_analytics_job- Sibling modules:
terraform-azurerm-stream-analytics-job,terraform-azurerm-stream-analytics-function-javascript-udf,terraform-azurerm-stream-analytics-cluster,terraform-azurerm-resource-group - This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."