Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions RELEASE_NOTES.md
Comment thread
ninjarobot marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ Release Notes
=============

## 1.9.30
* Container Apps: Update `Microsoft.App` ARM resource API versions for container apps, managed environments, environment storages, and Dapr components to the latest stable `2026-07-01`.
* Container Apps: Add a `serviceBusScaleRule` builder for `add_servicebus_scale_rule` so Service Bus scale rules no longer need direct record construction.
* Container Apps: Add required `Namespace` field to `ServiceBusScaleRule` to associate the scale rule with the correct Service Bus namespace.
* App Configuration: Add support for Azure App Configuration stores, key-value items, and feature flags.

## 1.9.29
Expand Down
30 changes: 27 additions & 3 deletions docs/content/api-overview/resources/container-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ weight: 3
#### Overview
The Container Apps builder is used to create Azure Container Apps.

* Container Environment (`Microsoft.Web/kubeEnvironments`)
* Container App (`Microsoft.Web/containerApps`)
* Container Environment (`Microsoft.App/managedEnvironments`)
* Container App (`Microsoft.App/containerApps`)

Farmer currently emits Azure Container Apps resources using the latest stable `2026-07-01` Microsoft.App ARM API version.

### Turn on Resource Provider
Before you deploy your container app, you need to turn on the Container Apps resource provider in your Azure subscription.

Get sure you have the following providers registered: `Microsoft.Kubernetes` and `Microsoft.ContainerService`.
Make sure you have the `Microsoft.App` provider registered.

#### Container Environment Builder
The Container Environment builder (`containerEnvironment`) defines settings for the Kubernetes environment that hosts the container apps.
Expand Down Expand Up @@ -75,6 +77,28 @@ The Container App Builder supports a number of KEDA scale rules out of the box:
| add_queue_scale_rule | Adds a scale rule for Azure Storage Queue length. |
| add_custom_scale_rule | Adds a custom scale rule. Provide an object that matches the KEDA specification. |

The `add_servicebus_scale_rule` keyword takes a name and a `ServiceBusScaleRule`. Prefer the `serviceBusScaleRule` builder so callers are insulated from future record shape changes:

```fsharp
add_servicebus_scale_rule "sb-keda-scale" (
serviceBusScaleRule {
queue_name "wishrequests"
servicebus_namespace "servicebus"
message_count 5
secret_ref "servicebusconnectionkey"
}
)
```

The builder supports the following fields:

| Field | Purpose |
|-|-|
| queue_name | The name of the Service Bus queue to monitor. |
| servicebus_namespace | The Service Bus namespace to associate with the scale rule. |
| message_count | The number of messages in the queue that triggers scaling. |
| secret_ref | The name of the secret containing the Service Bus connection string. |

> The Azure Storage Queue Scale Rule integration is "smart" - provide a reference to the storage account, queue name and length threshold; all appropriate settings and secrets will be automatically configured for you.

#### Container Builder
Expand Down
16 changes: 9 additions & 7 deletions src/Farmer/Arm/App.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ open System
open Farmer.ContainerApp
open Farmer

let containerApps = ResourceType("Microsoft.App/containerApps", "2022-03-01")
let containerApps = ResourceType("Microsoft.App/containerApps", "2026-07-01")

let managedEnvironments =
ResourceType("Microsoft.App/managedEnvironments", "2022-03-01")
ResourceType("Microsoft.App/managedEnvironments", "2026-07-01")

let storages =
ResourceType("Microsoft.App/managedEnvironments/storages", "2022-03-01")
ResourceType("Microsoft.App/managedEnvironments/storages", "2026-07-01")

let daprComponents =
ResourceType("Microsoft.App/managedEnvironments/daprComponents", "2022-10-01")
ResourceType("Microsoft.App/managedEnvironments/daprComponents", "2026-07-01")

open Farmer.ContainerAppValidation
open Farmer.Identity
Expand Down Expand Up @@ -417,6 +417,7 @@ type ContainerApp = {
metadata = {|
queueName = settings.QueueName
messageCount = string settings.MessageCount
``namespace`` = settings.Namespace
|}
auth = [|
{|
Expand Down Expand Up @@ -525,10 +526,7 @@ type ManagedEnvironment = {

member this.JsonModel = {|
managedEnvironments.Create(this.Name, this.Location, this.Dependencies, this.Tags) with
kind = "containerenvironment"
properties = {|
``type`` = "managed"
internalLoadBalancerEnabled = this.InternalLoadBalancerState.AsBoolean
daprAIInstrumentationKey =
this.AppInsightsInstrumentationKey
|> Option.map (fun key -> key.Eval())
Expand All @@ -540,5 +538,9 @@ type ManagedEnvironment = {
sharedKey = LogAnalytics.getPrimarySharedKey(this.LogAnalytics).Eval()
|}
|}
vnetConfiguration =
match this.InternalLoadBalancerState with
| Enabled -> box {| ``internal`` = true |}
| Disabled -> box {| |}
|}
|}
27 changes: 27 additions & 0 deletions src/Farmer/Builders/Builders.ContainerApps.fs
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,32 @@ type ContainerBuilder() =
)
}

type ServiceBusScaleRuleBuilder() =
member _.Yield _ = {
QueueName = ""
Namespace = ""
MessageCount = 0
SecretRef = ""
}

[<CustomOperation "queue_name">]
member _.QueueName(state: ServiceBusScaleRule, queueName: string) = { state with QueueName = queueName }

[<CustomOperation "servicebus_namespace">]
member _.Namespace(state: ServiceBusScaleRule, serviceBusNamespace: string) = {
state with
Namespace = serviceBusNamespace
}

[<CustomOperation "message_count">]
member _.MessageCount(state: ServiceBusScaleRule, messageCount: int) = {
state with
MessageCount = messageCount
}

[<CustomOperation "secret_ref">]
member _.SecretRef(state: ServiceBusScaleRule, secretRef: string) = { state with SecretRef = secretRef }

type DaprComponentBuilder() =
member _.Yield _ = {
Name = ResourceName.Empty
Expand Down Expand Up @@ -878,4 +904,5 @@ type DaprComponentBuilder() =
let containerEnvironment = ContainerEnvironmentBuilder()
let containerApp = ContainerAppBuilder()
let container = ContainerBuilder()
let serviceBusScaleRule = ServiceBusScaleRuleBuilder()
let daprComponent = DaprComponentBuilder()
1 change: 1 addition & 0 deletions src/Farmer/Common.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4136,6 +4136,7 @@ module ContainerApp =

type ServiceBusScaleRule = {
QueueName: string
Namespace: string
Comment thread
ninjarobot marked this conversation as resolved.
MessageCount: int
SecretRef: string
}
Expand Down
60 changes: 52 additions & 8 deletions src/Tests/ContainerApps.fs

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should one of the tests check that the value is set correctly?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added assertions to check that queueName, messageCount, and namespace are all correctly serialized in the JSON output (commit 47bfd399).

Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,14 @@ let fullContainerAppDeployment =
add_env_variable "ServiceBusQueueName" "wishrequests"
add_secret_parameter "servicebusconnectionkey"

add_servicebus_scale_rule "sb-keda-scale" {
QueueName = "wishrequests"
MessageCount = 5
SecretRef = "servicebusconnectionkey"
}
add_servicebus_scale_rule
"sb-keda-scale"
(serviceBusScaleRule {
queue_name "wishrequests"
message_count 5
secret_ref "servicebusconnectionkey"
servicebus_namespace "servicebus"
})
}
containerApp {
name "azurequeue"
Expand Down Expand Up @@ -192,9 +195,15 @@ let tests =
"Incorrect type for kuberenetes environment"

Expect.equal
(kubeEnv.["kind"] |> string)
"containerenvironment"
"Incorrect kind for kuberenetes environment"
(kubeEnv.["apiVersion"] |> string)
"2026-07-01"
"Incorrect API version for kuberenetes environment"

Expect.isNull (kubeEnv.SelectToken "kind") "Managed environment should not emit kind for 2026-07-01"

Expect.isNotNull
(kubeEnv.SelectToken("properties.vnetConfiguration"))
"Managed environment should emit vnetConfiguration for 2026-07-01"

let kubeEnvAppLogConfig =
jobj.SelectToken("resources[?(@.name=='kubecontainerenv')].properties.appLogsConfiguration")
Expand Down Expand Up @@ -224,6 +233,8 @@ let tests =
"Microsoft.App/managedEnvironments/daprComponents"
"Incorrect type for dapr component"

Expect.equal (daprComponent["apiVersion"] |> string) "2026-07-01" "Incorrect API version for dapr component"

let daprComponentProperties = daprComponent["properties"]

Expect.equal
Expand Down Expand Up @@ -265,6 +276,11 @@ let tests =
"Microsoft.App/containerApps"
"Incorrect type for containerApps"

Expect.equal
(httpContainerApp.["apiVersion"] |> string)
"2026-07-01"
"Incorrect API version for containerApps"

Expect.equal (httpContainerApp.["kind"] |> string) "containerapp" "Incorrect kind for containerApps"

let ingress = httpContainerApp.SelectToken("properties.configuration.ingress")
Expand Down Expand Up @@ -361,6 +377,26 @@ let tests =
"/certs"
"Incorrect container volume mount"

let sbScaleRuleMetadata =
serviceBusContainerApp.SelectToken("properties.template.scale.rules[0].custom.metadata")

Expect.isNotNull sbScaleRuleMetadata "service bus scale rule metadata was null"

Expect.equal
(sbScaleRuleMetadata["queueName"] |> string)
"wishrequests"
"Incorrect service bus scale rule queueName"

Expect.equal
(sbScaleRuleMetadata["messageCount"] |> string)
"5"
"Incorrect service bus scale rule messageCount"

Expect.equal
(sbScaleRuleMetadata["namespace"] |> string)
"servicebus"
"Incorrect service bus scale rule namespace"

let azureQueueContainerApp = jobj.SelectToken("resources[?(@.name=='azurequeue')]")
Expect.isNotNull azureQueueContainerApp "resources[?(@.name=='azurequeue')] was null"

Expand Down Expand Up @@ -410,6 +446,14 @@ let tests =
|> List.find (fun r -> r.ResourceId.Name.Value = "certs-v")
:?> Farmer.Arm.App.ManagedEnvironmentStorage

let certsStorageJson =
jobj.SelectToken("resources[?(@.name=='kubecontainerenv/certs-v')]")

Expect.equal
(certsStorageJson.["apiVersion"] |> string)
"2026-07-01"
"Incorrect API version for managed environment storage"

Expect.equal
certsStorage.AzureFile.AccessMode
StorageAccessMode.ReadOnly
Expand Down