From 6b4df76252ca0e8691a3356ea2b1787eda304fba Mon Sep 17 00:00:00 2001 From: Bryan921105 Date: Wed, 23 Aug 2023 00:06:31 +0900 Subject: [PATCH 1/5] feat(controller): add azure functions app helper --- .../util/azure/azure_funtions_app_helper.rs | 114 ++++++++++++++++++ core/wave-autoscale/src/util/azure/mod.rs | 1 + 2 files changed, 115 insertions(+) create mode 100644 core/wave-autoscale/src/util/azure/azure_funtions_app_helper.rs diff --git a/core/wave-autoscale/src/util/azure/azure_funtions_app_helper.rs b/core/wave-autoscale/src/util/azure/azure_funtions_app_helper.rs new file mode 100644 index 00000000..3ee7cee8 --- /dev/null +++ b/core/wave-autoscale/src/util/azure/azure_funtions_app_helper.rs @@ -0,0 +1,114 @@ +use super::*; +use reqwest::{Client, Response}; + +#[derive(Clone)] +pub struct AzureFunctionsPatchAppSetting { + pub azure_credential: AzureCredential, + pub subscription_id: String, + pub resource_group_name: String, + pub app_name: String, + pub payload: Option, +} + +// https://learn.microsoft.com/en-us/rest/api/appservice/web-apps/update +pub async fn call_patch_azure_functions_app( + azure_functions_app_setting: AzureFunctionsPatchAppSetting, +) -> Result { + Client::new() + .patch(format!( + "https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Web/sites/{app_name}?api-version=2022-03-01", + subscriptionId = azure_functions_app_setting.subscription_id, + resourceGroupName = azure_functions_app_setting.resource_group_name, + app_name = azure_functions_app_setting.app_name + )) + .bearer_auth(get_azure_credential_token(azure_functions_app_setting.azure_credential).await.unwrap_or("".to_string())) + .json(&azure_functions_app_setting.payload) + .send() + .await +} + +#[cfg(test)] +mod test { + use super::*; + + fn get_test_env_data() -> (AzureCredential, String) { + let azure_credential = AzureCredential { + client_id: None, + client_secret: None, + tenant_id: None, + }; + let subscription_id = std::env::var("AZURE_SUBSCRIPTION_ID").unwrap(); + (azure_credential, subscription_id) + } + + #[ignore] + #[tokio::test] + async fn test_call_azure_functions_app_for_consumption_plan() { + let azure_functions_app_setting = AzureFunctionsPatchAppSetting { + azure_credential: get_test_env_data().0, + subscription_id: get_test_env_data().1, + resource_group_name: "test-azure-functions".to_string(), + app_name: "functions-app-3".to_string(), + payload: Some(serde_json::json!({ + "properties": { + "siteConfig" : { + "functionAppScaleLimit": 10, + }, + }, + })), + }; + + let response = call_patch_azure_functions_app(azure_functions_app_setting) + .await + .unwrap(); + + let status = response.status(); + let body = response.text().await.unwrap_or("".to_string()); + println!( + "test_call_azure_functions_app_for_consumption_plan response status: {:?}", + status + ); + println!( + "test_call_azure_functions_app_for_consumption_plan response body: {:?}", + body + ); + + assert!(status == reqwest::StatusCode::OK); + } + + #[ignore] + #[tokio::test] + async fn test_call_azure_functions_app_for_premium_plan() { + let azure_functions_app_setting = AzureFunctionsPatchAppSetting { + azure_credential: get_test_env_data().0, + subscription_id: get_test_env_data().1, + resource_group_name: "test-azure-functions".to_string(), + app_name: "functions-app-2".to_string(), + payload: Some(serde_json::json!({ + "properties": { + "siteConfig" : { + "functionAppScaleLimit": 10, + "minimumElasticInstanceCount": 5, + }, + }, + })), + }; + + let response = call_patch_azure_functions_app(azure_functions_app_setting) + .await + .unwrap(); + + let status = response.status(); + let body = response.text().await.unwrap_or("".to_string()); + println!( + "test_call_azure_functions_app_for_premium_plan response status: {:?}", + status + ); + println!( + "test_call_azure_functions_app_for_premium_plan response body: {:?}", + body + ); + + assert!(status == reqwest::StatusCode::OK); + } +} diff --git a/core/wave-autoscale/src/util/azure/mod.rs b/core/wave-autoscale/src/util/azure/mod.rs index 0409a738..b24a3f2a 100644 --- a/core/wave-autoscale/src/util/azure/mod.rs +++ b/core/wave-autoscale/src/util/azure/mod.rs @@ -4,6 +4,7 @@ use azure_core; use azure_core::auth::TokenCredential; use azure_identity::client_credentials_flow; use log::error; +pub mod azure_funtions_app_helper; #[derive(Clone)] pub struct AzureCredential { From 415f59633aef4ca130528696e8e5f9040c32e3f5 Mon Sep 17 00:00:00 2001 From: Bryan921105 Date: Wed, 23 Aug 2023 00:54:05 +0900 Subject: [PATCH 2/5] feat(controller): add azure funtions app scaling component --- .../scaling_component/azure_functions_app.rs | 296 ++++++++++++++++++ .../src/scaling_component/mod.rs | 5 + 2 files changed, 301 insertions(+) create mode 100644 core/wave-autoscale/src/scaling_component/azure_functions_app.rs diff --git a/core/wave-autoscale/src/scaling_component/azure_functions_app.rs b/core/wave-autoscale/src/scaling_component/azure_functions_app.rs new file mode 100644 index 00000000..1e4f15ab --- /dev/null +++ b/core/wave-autoscale/src/scaling_component/azure_functions_app.rs @@ -0,0 +1,296 @@ +use super::super::util::azure::azure_funtions_app_helper::{ + call_patch_azure_functions_app, AzureFunctionsPatchAppSetting, +}; +use super::ScalingComponent; +use crate::util::azure::AzureCredential; +use anyhow::{Ok, Result}; +use async_trait::async_trait; +use data_layer::ScalingComponentDefinition; + +use std::collections::HashMap; + +pub struct AzureFunctionsAppScalingComponent { + definition: ScalingComponentDefinition, +} + +impl AzureFunctionsAppScalingComponent { + // Static variables + pub const SCALING_KIND: &'static str = "azure-functions"; + + // Functions + pub fn new(definition: ScalingComponentDefinition) -> Self { + AzureFunctionsAppScalingComponent { definition } + } +} + +#[async_trait] +impl ScalingComponent for AzureFunctionsAppScalingComponent { + fn get_scaling_component_kind(&self) -> &str { + &self.definition.component_kind + } + + fn get_id(&self) -> &str { + &self.definition.id + } + + async fn apply(&self, params: HashMap) -> Result<()> { + let metadata: HashMap = self.definition.metadata.clone(); + + if let ( + Some(serde_json::Value::String(client_id)), + Some(serde_json::Value::String(client_secret)), + Some(serde_json::Value::String(tenant_id)), + Some(serde_json::Value::String(subscription_id)), + Some(serde_json::Value::String(resource_group_name)), + Some(serde_json::Value::String(app_name)), + min_instance_count, + max_instance_count, + ) = ( + metadata.get("client_id"), + metadata.get("client_secret"), + metadata.get("tenant_id"), + metadata.get("subscription_id"), + metadata.get("resource_group_name"), + metadata.get("app_name"), + params + .get("min_instance_count") + .and_then(serde_json::Value::as_u64) + .map(|v| v as u32), + params + .get("max_instance_count") + .and_then(serde_json::Value::as_u64) + .map(|v| v as u32), + ) { + // Call patch azure functions app api + let azure_credential = AzureCredential { + client_id: Some(client_id.to_string()), + client_secret: Some(client_secret.to_string()), + tenant_id: Some(tenant_id.to_string()), + }; + let azure_functions_app_setting = AzureFunctionsPatchAppSetting { + azure_credential: azure_credential.clone(), + subscription_id: subscription_id.to_string(), + resource_group_name: resource_group_name.to_string(), + app_name: app_name.to_string(), + payload: Some(create_payload( + min_instance_count + .map(|value| value.to_string()) + .as_deref() + .or(None), + max_instance_count + .map(|value| value.to_string()) + .as_deref() + .or(None), + )), + }; + let result = call_patch_azure_functions_app(azure_functions_app_setting).await; + if result.is_err() { + return Err(anyhow::anyhow!(serde_json::json!({ + "message": "API call error", + "code": "500", + "extras": result.unwrap_err().is_body().to_string() + }))); + } + let result = result.unwrap(); + let result_status_code = result.status(); + let result_body: String = match result.text().await { + core::result::Result::Ok(result_body) => result_body, + Err(_error) => { + return Err(anyhow::anyhow!(serde_json::json!({ + "message": "API call error", + "code": "500", + "extras": "Not found response text", + }))); + } + }; + if !result_status_code.is_success() { + log::error!("API call error: {:?}", &result_body); + let json = serde_json::json!({ + "message": "API call error", + "code": result_status_code.as_str(), + "extras": result_body + }); + return Err(anyhow::anyhow!(json)); + } + + Ok(()) + } else { + Err(anyhow::anyhow!("Invalid metadata")) + } + } +} + +fn create_payload( + min_instance_count: Option<&str>, + max_instance_count: Option<&str>, +) -> serde_json::Value { + match (min_instance_count, max_instance_count) { + (Some(min_instance_count), Some(max_instance_count)) => { + serde_json::json!({ + "properties": { + "siteConfig": { + "functionAppScaleLimit": max_instance_count, + "minimumElasticInstanceCount": min_instance_count + } + } + }) + } + (_, Some(max_instance_count)) => { + serde_json::json!({ + "properties": { + "siteConfig": { + "functionAppScaleLimit": max_instance_count + } + } + }) + } + _ => serde_json::Value::Null, + } +} + +#[cfg(test)] +mod test { + use super::AzureFunctionsAppScalingComponent; + use crate::scaling_component::ScalingComponent; + use data_layer::ScalingComponentDefinition; + use std::collections::HashMap; + + #[ignore] + #[tokio::test] + async fn apply_call_patch_azure_functions_app_for_consumption_plan() { + let metadata: HashMap = vec![ + (String::from("client_id"), serde_json::json!("CLIENT_ID")), + ( + String::from("client_secret"), + serde_json::json!("CLIENT_SECRET"), + ), + (String::from("tenant_id"), serde_json::json!("TENANT_ID")), + ( + String::from("subscription_id"), + serde_json::json!("SUBSCRIPTION_ID"), + ), + ( + String::from("resource_group_name"), + serde_json::json!("RESOURCE_GROUP_NAME"), + ), + (String::from("app_name"), serde_json::json!("APP_NAME")), + ] + .into_iter() + .collect(); + let params: HashMap = + vec![(String::from("max_instance_count"), serde_json::json!(5))] + .into_iter() + .collect(); + let scaling_definition = ScalingComponentDefinition { + kind: data_layer::types::object_kind::ObjectKind::ScalingComponent, + db_id: String::from("db-id"), + id: String::from("scaling-id"), + component_kind: String::from("azure-functions"), + metadata, + }; + let azure_functions_app_scaling_component: Result<(), anyhow::Error> = + AzureFunctionsAppScalingComponent::new(scaling_definition) + .apply(params) + .await; + + println!( + "azure_functions_app_scaling_component: {:?}", + azure_functions_app_scaling_component + ); + assert!(azure_functions_app_scaling_component.is_ok()); + } + + #[ignore] + #[tokio::test] + async fn apply_call_patch_azure_functions_app_for_premium_plan() { + let metadata: HashMap = vec![ + (String::from("client_id"), serde_json::json!("CLIENT_ID")), + ( + String::from("client_secret"), + serde_json::json!("CLIENT_SECRET"), + ), + (String::from("tenant_id"), serde_json::json!("TENANT_ID")), + ( + String::from("subscription_id"), + serde_json::json!("SUBSCRIPTION_ID"), + ), + ( + String::from("resource_group_name"), + serde_json::json!("RESOURCE_GROUP_NAME"), + ), + (String::from("app_name"), serde_json::json!("APP_NAME")), + ] + .into_iter() + .collect(); + let params: HashMap = vec![ + (String::from("min_instance_count"), serde_json::json!(2)), + (String::from("max_instance_count"), serde_json::json!(5)), + ] + .into_iter() + .collect(); + let scaling_definition = ScalingComponentDefinition { + kind: data_layer::types::object_kind::ObjectKind::ScalingComponent, + db_id: String::from("db-id"), + id: String::from("scaling-id"), + component_kind: String::from("azure-functions"), + metadata, + }; + let azure_functions_app_scaling_component: Result<(), anyhow::Error> = + AzureFunctionsAppScalingComponent::new(scaling_definition) + .apply(params) + .await; + + println!( + "azure_functions_app_scaling_component: {:?}", + azure_functions_app_scaling_component + ); + assert!(azure_functions_app_scaling_component.is_ok()); + } + + #[ignore] + #[tokio::test] + async fn apply_error_call_patch_azure_functions_app_for_consumption_plan() { + let metadata: HashMap = vec![ + (String::from("client_id"), serde_json::json!("CLIENT_ID")), + ( + String::from("client_secret"), + serde_json::json!("CLIENT_SECRET"), + ), + (String::from("tenant_id"), serde_json::json!("TENANT_ID")), + ( + String::from("subscription_id"), + serde_json::json!("SUBSCRIPTION_ID"), + ), + ( + String::from("resource_group_name"), + serde_json::json!("RESOURCE_GROUP_NAME"), + ), + (String::from("app_name"), serde_json::json!("APP_NAME")), + ] + .into_iter() + .collect(); + let params: HashMap = vec![ + (String::from("min_instance_count"), serde_json::json!(2)), + (String::from("max_instance_count"), serde_json::json!(5)), + ] + .into_iter() + .collect(); + let scaling_definition = ScalingComponentDefinition { + kind: data_layer::types::object_kind::ObjectKind::ScalingComponent, + db_id: String::from("db-id"), + id: String::from("scaling-id"), + component_kind: String::from("azure-functions"), + metadata, + }; + let azure_functions_app_scaling_component: Result<(), anyhow::Error> = + AzureFunctionsAppScalingComponent::new(scaling_definition) + .apply(params) + .await; + + println!( + "azure_functions_app_scaling_component: {:?}", + azure_functions_app_scaling_component + ); + assert!(azure_functions_app_scaling_component.is_ok()); + } +} diff --git a/core/wave-autoscale/src/scaling_component/mod.rs b/core/wave-autoscale/src/scaling_component/mod.rs index bee0372c..12155799 100644 --- a/core/wave-autoscale/src/scaling_component/mod.rs +++ b/core/wave-autoscale/src/scaling_component/mod.rs @@ -7,6 +7,7 @@ pub mod amazon_emr_ec2; pub mod aws_ec2_autoscaling; pub mod aws_ecs_service_scaling; pub mod aws_lambda_function; +pub mod azure_functions_app; pub mod azure_vmss_autoscaling; pub mod gcp_mig_autoscaling; pub mod google_cloud_functions_instance; @@ -17,6 +18,7 @@ use self::{ amazon_emr_ec2::EMREC2AutoScalingComponent, aws_ec2_autoscaling::EC2AutoScalingComponent, aws_ecs_service_scaling::ECSServiceScalingComponent, aws_lambda_function::LambdaFunctionScalingComponent, + azure_functions_app::AzureFunctionsAppScalingComponent, azure_vmss_autoscaling::VMSSAutoScalingComponent, gcp_mig_autoscaling::MIGAutoScalingComponent, google_cloud_functions_instance::CloudFunctionsInstanceScalingComponent, google_cloud_run_service::CloudRunServiceScalingComponent, @@ -90,6 +92,9 @@ impl ScalingComponentManager { CloudRunServiceScalingComponent::SCALING_KIND => Ok(Box::new( CloudRunServiceScalingComponent::new(cloned_defintion), )), + AzureFunctionsAppScalingComponent::SCALING_KIND => Ok(Box::new( + AzureFunctionsAppScalingComponent::new(cloned_defintion), + )), _ => Err(anyhow::anyhow!("Unknown trigger kind")), } } From 39fa837b16a7ab3c2d625e8b6d53547379802aeb Mon Sep 17 00:00:00 2001 From: Bryan921105 Date: Wed, 23 Aug 2023 01:04:19 +0900 Subject: [PATCH 3/5] feat(controller): add azure functions app test case file --- .../tests/azure_functions_app.rs | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 core/wave-autoscale/tests/azure_functions_app.rs diff --git a/core/wave-autoscale/tests/azure_functions_app.rs b/core/wave-autoscale/tests/azure_functions_app.rs new file mode 100644 index 00000000..1ac7a0d7 --- /dev/null +++ b/core/wave-autoscale/tests/azure_functions_app.rs @@ -0,0 +1,122 @@ +mod google_cloud_run_service_test { + use std::collections::HashMap; + + use anyhow::Result; + + use data_layer::types::object_kind::ObjectKind::ScalingComponent; + use data_layer::ScalingComponentDefinition; + use wave_autoscale::scaling_component::azure_functions_app::AzureFunctionsAppScalingComponent; + use wave_autoscale::scaling_component::ScalingComponentManager; + + #[tokio::test] + #[ignore] + async fn apply_maximum_scale_out_limit_for_consumption_plan() -> Result<()> { + let metadata: HashMap = vec![ + (String::from("client_id"), serde_json::json!("CLIENT_ID")), + ( + String::from("client_secret"), + serde_json::json!("CLIENT_SECRET"), + ), + (String::from("tenant_id"), serde_json::json!("TENANT_ID")), + ( + String::from("subscription_id"), + serde_json::json!("SUBSCRIPTION_ID"), + ), + ( + String::from("resource_group_name"), + serde_json::json!("RESOURCE_GROUP_NAME"), + ), + (String::from("app_name"), serde_json::json!("APP_NAME")), + ] + .into_iter() + .collect(); + let scaling_component_definitions = vec![ScalingComponentDefinition { + kind: ScalingComponent, + db_id: String::from("db_id"), + id: String::from("scaling_component_azure_functions_app"), + component_kind: String::from("azure-functions-app"), + metadata, + }]; + + let mut scaling_component_manager = ScalingComponentManager::new(); + scaling_component_manager.add_definitions(scaling_component_definitions); + + if let Some(scaling_component) = + scaling_component_manager.get_scaling_component("scaling_component_azure_functions_app") + { + let name = scaling_component.get_scaling_component_kind(); + assert!( + name == AzureFunctionsAppScalingComponent::SCALING_KIND, + "Unexpected" + ); + } else { + assert!(false, "No scaling component found") + } + + let params: HashMap = + vec![(String::from("max_instance_count"), serde_json::json!(200))] + .into_iter() + .collect(); + + scaling_component_manager + .apply_to("scaling_component_azure_functions_app", params) + .await + } + + #[tokio::test] + #[ignore] + async fn apply_maximum_always_ready_instance_for_premium_plan() -> Result<()> { + let metadata: HashMap = vec![ + (String::from("client_id"), serde_json::json!("CLIENT_ID")), + ( + String::from("client_secret"), + serde_json::json!("CLIENT_SECRET"), + ), + (String::from("tenant_id"), serde_json::json!("TENANT_ID")), + ( + String::from("subscription_id"), + serde_json::json!("SUBSCRIPTION_ID"), + ), + ( + String::from("resource_group_name"), + serde_json::json!("RESOURCE_GROUP_NAME"), + ), + (String::from("app_name"), serde_json::json!("APP_NAME")), + ] + .into_iter() + .collect(); + let scaling_component_definitions = vec![ScalingComponentDefinition { + kind: ScalingComponent, + db_id: String::from("db_id"), + id: String::from("scaling_component_azure_functions_app"), + component_kind: String::from("azure-functions-app"), + metadata, + }]; + + let mut scaling_component_manager = ScalingComponentManager::new(); + scaling_component_manager.add_definitions(scaling_component_definitions); + + if let Some(scaling_component) = + scaling_component_manager.get_scaling_component("scaling_component_azure_functions_app") + { + let name = scaling_component.get_scaling_component_kind(); + assert!( + name == AzureFunctionsAppScalingComponent::SCALING_KIND, + "Unexpected" + ); + } else { + assert!(false, "No scaling component found") + } + + let params: HashMap = vec![ + (String::from("min_instance_count"), serde_json::json!(20)), + (String::from("max_instance_count"), serde_json::json!(200)), + ] + .into_iter() + .collect(); + + scaling_component_manager + .apply_to("scaling_component_azure_functions_app", params) + .await + } +} From 3524061e530e034440376bf7e1443a65b8b1a512 Mon Sep 17 00:00:00 2001 From: Bryan921105 Date: Wed, 23 Aug 2023 01:16:08 +0900 Subject: [PATCH 4/5] feat(controller): add azure functions app plan file --- .../plan_cloudwatch_azure_functions_app.yaml | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 core/wave-autoscale/tests/yaml/plan_cloudwatch_azure_functions_app.yaml diff --git a/core/wave-autoscale/tests/yaml/plan_cloudwatch_azure_functions_app.yaml b/core/wave-autoscale/tests/yaml/plan_cloudwatch_azure_functions_app.yaml new file mode 100644 index 00000000..bff67036 --- /dev/null +++ b/core/wave-autoscale/tests/yaml/plan_cloudwatch_azure_functions_app.yaml @@ -0,0 +1,62 @@ +--- +kind: Metric +id: cloudwatch_concurrent_executions_cnt +collector: telegraf +metric_kind: cloudwatch +metadata: + region: ap-northeast-2 + # AWS Credentials using the local AWS credentials file(~/.aws/credentials) + profile: "default" + # Requested CloudWatch aggregation Period (required) + # Must be a multiple of 60s. + period: "2m" + # Collection Delay (required) + # Must account for metrics availability via CloudWatch API + delay: "2m" + # Recommended: use metric 'interval' that is a multiple of 'period' to avoid + # gaps or overlap in pulled data + interval: "1m" + namespaces: ["MicroSoft/AzureFunctions"] + metrics: + - names: ["Invocations"] + statistic_include: ["sample_count"] + dimensions: + - name: AppName + value: "functions-app-2" +--- +kind: ScalingComponent +id: scaling_component_azure_functions_app +component_kind: azure-functions-app +metadata: + client_id: CLIENT_ID + client_secret: CLIENT_SECRET + tenant_id: TENANT_ID + subscription_id: SUBSCRIPTION_ID + resource_group_name: RESOURCE_GROUP_NAME + app_name: functions-app-2 +--- +kind: ScalingPlan +id: scaling_plan_azure_functions_app +title: "Scaling Plan for Azure Functions App for Premium Plan" +plans: + - id: scale-out-plan-1 + description: "Scale out if concurrent executions value is greater than 10." + # JavaScript expression that returns a boolean value. + expression: "cloudwatch_concurrent_executions_cnt >= 10" + # Higher priority values will be checked first. + priority: 1 + scaling_components: + - component_id: scaling_component_azure_functions_app + # JavaScript expression that returns an integer. + min_instance_count: 10 + max_instance_count: 30 + - id: scale-out-plan-2 + description: "Scale out if concurrent executions value is greater than 2." + # JavaScript expression that returns a boolean value. + expression: "cloudwatch_concurrent_executions_cnt >= 2" + priority: 2 + scaling_components: + - component_id: scaling_component_azure_functions_app + # JavaScript expression that returns an integer. + min_instance_count: 2 + max_instance_count: 5 From 7529dc7551973f228fc8aadbed380e7465c6a428 Mon Sep 17 00:00:00 2001 From: Bryan921105 Date: Wed, 23 Aug 2023 16:04:03 +0900 Subject: [PATCH 5/5] feat(controller): add comment --- .../src/scaling_component/azure_functions_app.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/wave-autoscale/src/scaling_component/azure_functions_app.rs b/core/wave-autoscale/src/scaling_component/azure_functions_app.rs index 1e4f15ab..6890363f 100644 --- a/core/wave-autoscale/src/scaling_component/azure_functions_app.rs +++ b/core/wave-autoscale/src/scaling_component/azure_functions_app.rs @@ -1,8 +1,8 @@ -use super::super::util::azure::azure_funtions_app_helper::{ - call_patch_azure_functions_app, AzureFunctionsPatchAppSetting, +use super::super::util::azure::{ + azure_funtions_app_helper::{call_patch_azure_functions_app, AzureFunctionsPatchAppSetting}, + AzureCredential, }; use super::ScalingComponent; -use crate::util::azure::AzureCredential; use anyhow::{Ok, Result}; use async_trait::async_trait; use data_layer::ScalingComponentDefinition; @@ -120,6 +120,7 @@ impl ScalingComponent for AzureFunctionsAppScalingComponent { } } +// Create payload to patch azure functions app api fn create_payload( min_instance_count: Option<&str>, max_instance_count: Option<&str>,