diff --git a/README.md b/README.md index 67b13829..cd1e0ca9 100644 --- a/README.md +++ b/README.md @@ -408,7 +408,7 @@ proxy_url = VALUE | [function\_app\_storage\_account\_prefix](#input\_function\_app\_storage\_account\_prefix) | Weka storage account name prefix | `string` | `"weka"` | no | | [function\_app\_subnet\_delegation\_cidr](#input\_function\_app\_subnet\_delegation\_cidr) | Subnet delegation enables you to designate a specific subnet for an Azure PaaS service. | `string` | `"10.0.1.0/25"` | no | | [function\_app\_subnet\_delegation\_id](#input\_function\_app\_subnet\_delegation\_id) | Required to specify if subnet\_name were used to specify pre-defined subnets for weka. Function subnet delegation requires an additional subnet, and in the case of pre-defined networking this one also should be pre-defined | `string` | `""` | no | -| [function\_app\_version](#input\_function\_app\_version) | Function app code version (hash) | `string` | `"9611fb1feebc0c4e3f8b34671fa909fe"` | no | +| [function\_app\_version](#input\_function\_app\_version) | Function app code version (hash) | `string` | `"abe83f3f0224c29630262452d616dffd"` | no | | [get\_weka\_io\_token](#input\_get\_weka\_io\_token) | The token to download the Weka release from get.weka.io. | `string` | `""` | no | | [hotspare](#input\_hotspare) | Number of hotspares to set on weka cluster. Refer to https://docs.weka.io/overview/ssd-capacity-management#hot-spare | `number` | `1` | no | | [install\_cluster\_dpdk](#input\_install\_cluster\_dpdk) | Install weka cluster with DPDK | `bool` | `true` | no | diff --git a/function-app/code/common/common.go b/function-app/code/common/common.go index 5d01e7b7..e13a8c00 100644 --- a/function-app/code/common/common.go +++ b/function-app/code/common/common.go @@ -2,7 +2,9 @@ package common import ( "context" + "crypto/sha256" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -962,7 +964,7 @@ func RetrySetDeletionProtectionAndReport( counter++ // deletion protection invoked by terminate function if maxAttempts == 0 { - msg := fmt.Sprintf("Deletion protection set authorization isn't ready, will retry on next scale down workflow") + msg := "Deletion protection set authorization isn't ready, will retry on next scale down workflow" ReportMsg(ctx, hostName, subscriptionId, resourceGroupName, stateContainerName, stateStorageName, "debug", msg) return } @@ -994,8 +996,11 @@ func GetWekaClusterPassword(ctx context.Context, keyVaultUri string) (password s return GetKeyVaultValue(ctx, keyVaultUri, "weka-password") } -func GetVmScaleSetName(prefix, clusterName string, version string) string { - return fmt.Sprintf("%s-%s-vmss-%s", prefix, clusterName, version) +func GetVmScaleSetName(prefix, clusterName string, version int) string { + if version == 0 { + return fmt.Sprintf("%s-%s-vmss", prefix, clusterName) + } + return fmt.Sprintf("%s-%s-vmss-v%d", prefix, clusterName, version) } func GetScaleSetInstanceIds(ctx context.Context, subscriptionId, resourceGroupName, vmScaleSetName string) (instanceIds []string, err error) { @@ -1111,12 +1116,19 @@ func ReadVmssConfig(ctx context.Context, storageName, containerName string) (vms if err != nil { return } + + // calculate hash of the config (used to identify vmss config changes) + // take first 10 chars of the hash + hash := sha256.Sum256(asByteArray) + hashStr := fmt.Sprintf("%x", hash) + err = json.Unmarshal(asByteArray, &vmssConfig) if err != nil { logger.Error().Err(err).Send() return } + vmssConfig.ConfigHash = hashStr[:10] return } @@ -1233,23 +1245,29 @@ func GetScaleSetNameWithLatestConfiguration(ctx context.Context, subscriptionId, return scaleSetName, nil } -func GetScaleSetsList(ctx context.Context, subscriptionId, resourceGroupName string, scaleSetNames []string) (scaleSets []*armcompute.VirtualMachineScaleSet, err error) { +func GetScaleSetsByVersion(ctx context.Context, subscriptionId, resourceGroupName string, vmssState *VMSSState) (map[int]*armcompute.VirtualMachineScaleSet, error) { logger := logging.LoggerFromCtx(ctx) - for _, scaleSetName := range scaleSetNames { + scaleSetNames := make([]string, 0) + scaleSetsByVersion := make(map[int]*armcompute.VirtualMachineScaleSet, len(vmssState.Versions)) + + for _, version := range vmssState.Versions { + scaleSetName := GetVmScaleSetName(vmssState.Prefix, vmssState.ClusterName, version) scaleSet, err := getScaleSet(ctx, subscriptionId, resourceGroupName, scaleSetName) if err != nil { // if scale set not found, ignore - if getErr, ok := err.(*azcore.ResponseError); ok && getErr.ErrorCode == "ResourceNotFound" { + var responseErr *azcore.ResponseError + if errors.As(err, &responseErr) && responseErr.ErrorCode == "ResourceNotFound" { continue } return nil, err } - scaleSets = append(scaleSets, scaleSet) + scaleSetsByVersion[version] = scaleSet + scaleSetNames = append(scaleSetNames, scaleSetName) } - logger.Info().Msgf("Found %d scale sets from list %v", len(scaleSets), scaleSetNames) - return + logger.Info().Msgf("Found %d scale sets from list %v", len(scaleSetsByVersion), scaleSetNames) + return scaleSetsByVersion, nil } func GetVmssConfig(ctx context.Context, resourceGroupName string, scaleSet *armcompute.VirtualMachineScaleSet) *VMSSConfig { @@ -1388,7 +1406,7 @@ func CreateOrUpdateVmss(ctx context.Context, subscriptionId, resourceGroupName, return } - config.Tags["version"] = configHash + config.Tags["config_hash"] = configHash config.Tags["config_applied_at"] = time.Now().Format(time.RFC3339) size := int64(vmssSize) forceDeletion := false diff --git a/function-app/code/common/vmss_config.go b/function-app/code/common/vmss_config.go index 0891933a..192ee2c5 100644 --- a/function-app/code/common/vmss_config.go +++ b/function-app/code/common/vmss_config.go @@ -1,8 +1,6 @@ package common import ( - "crypto/sha256" - "encoding/json" "fmt" "strings" @@ -84,30 +82,21 @@ type VMSSConfig struct { DataDisk DataDisk `json:"data_disk"` PrimaryNIC PrimaryNIC `json:"primary_nic"` SecondaryNICs SecondaryNICs `json:"secondary_nics"` -} - -func GetConfigHash(c VMSSConfig) (string, error) { - // calculate hash of the config (used to identify vmss config changes) - // take first 10 chars of the hash - jsonData, err := json.Marshal(c) - if err != nil { - return "", fmt.Errorf("cannot marshal vmss config: %v", err) - } - hash := sha256.Sum256(jsonData) - hashStr := fmt.Sprintf("%x", hash) - return hashStr[:10], nil + // ignore the following fields when marshaling to json + ConfigHash string `json:"-"` } // Compares two vmss configs - works with copies of VMSSConfig structs // NOTES: -// - does not compare "version" and "config_applied_at" tags, and names which include version +// - does not compare "config_hash" and "config_applied_at" tags, and names which include version func VmssConfigsDiff(old, new VMSSConfig) string { old.CustomData, new.CustomData = "", "" - old.Tags["version"], new.Tags["version"] = "", "" + old.Tags["config_hash"], new.Tags["config_hash"] = "", "" old.Tags["config_applied_at"], new.Tags["config_applied_at"] = "", "" old.ComputerNamePrefix, new.ComputerNamePrefix = "", "" old.Name, new.Name = "", "" + old.ConfigHash, new.ConfigHash = "", "" if len(old.PrimaryNIC.IPConfigurations) == len(new.PrimaryNIC.IPConfigurations) { for i := range old.PrimaryNIC.IPConfigurations { @@ -121,7 +110,7 @@ func VmssConfigsDiff(old, new VMSSConfig) string { old.OSDisk.SizeGB = nil } - return cmp.Diff(new, old) // arguments order: (want, got) + return cmp.Diff(old, new) // arguments order: (want, got) } func GetRefreshVmssName(outdatedVmssName string, currentVmssVersion uint16) string { @@ -133,12 +122,25 @@ func GetRefreshVmssName(outdatedVmssName string, currentVmssVersion uint16) stri } type VMSSState struct { - Prefix string `json:"prefix"` - ClusterName string `json:"cluster_name"` - Versions []string `json:"active_versions"` + Prefix string `json:"prefix"` + ClusterName string `json:"cluster_name"` + Versions []int `json:"active_versions"` } -func (q *VMSSState) AddVersion(item string) { +func (q *VMSSState) DeduceNextVersion() int { + if len(q.Versions) == 0 { + return 0 + } + maxVersion := q.Versions[0] + for _, v := range q.Versions { + if v > maxVersion { + maxVersion = v + } + } + return maxVersion + 1 +} + +func (q *VMSSState) AddVersion(item int) { // make sure version is added in the end of the queue // and there are no duplicates for i, v := range q.Versions { @@ -152,7 +154,7 @@ func (q *VMSSState) AddVersion(item string) { q.Versions = append(q.Versions, item) } -func (q *VMSSState) GetLatestVersion() string { +func (q *VMSSState) GetLatestVersion() int { return q.Versions[len(q.Versions)-1] } @@ -160,16 +162,7 @@ func (q *VMSSState) IsEmpty() bool { return len(q.Versions) == 0 } -func (q *VMSSState) ReplaceVersion(old, new string) { - for i, v := range q.Versions { - if v == old { - q.Versions[i] = new - return - } - } -} - -func (q *VMSSState) RemoveVersion(item string) error { +func (q *VMSSState) RemoveVersion(item int) error { for i, v := range q.Versions { // do not allow removing the last element of array if v == item && i != len(q.Versions)-1 { @@ -179,7 +172,7 @@ func (q *VMSSState) RemoveVersion(item string) error { return fmt.Errorf("cannot remove the latest version from the queue") } } - return fmt.Errorf("version %s not found in the queue", item) + return fmt.Errorf("version %d not found in the queue", item) } type VMSSStateVerbose struct { diff --git a/function-app/code/functions/clusterize/clusterize.go b/function-app/code/functions/clusterize/clusterize.go index 4582bba9..094ab4b1 100644 --- a/function-app/code/functions/clusterize/clusterize.go +++ b/function-app/code/functions/clusterize/clusterize.go @@ -176,14 +176,8 @@ func Clusterize(ctx context.Context, p ClusterizationParams) (clusterizeScript s instanceName := strings.Split(p.VmName, ":")[0] instanceId := common.GetScaleSetVmIndex(instanceName) - vmssState, err := common.ReadVmssState(ctx, p.StateStorageName, p.StateContainerName) - if err != nil { - err = fmt.Errorf("failed to read vmss state: %w", err) - logger.Error().Err(err).Send() - return - } - - vmScaleSetName := common.GetVmScaleSetName(p.Prefix, p.Cluster.ClusterName, vmssState.GetLatestVersion()) + version := 0 // on cluserization step we are sure that the vmss version is 0 as no refresh was done yet + vmScaleSetName := common.GetVmScaleSetName(p.Prefix, p.Cluster.ClusterName, version) vmName := p.VmName ip, err := common.GetPublicIp(ctx, p.SubscriptionId, p.ResourceGroupName, vmScaleSetName, p.Prefix, p.Cluster.ClusterName, instanceId) diff --git a/function-app/code/functions/scale_up/scale_up.go b/function-app/code/functions/scale_up/scale_up.go index 4bbc02f9..4f5a9175 100644 --- a/function-app/code/functions/scale_up/scale_up.go +++ b/function-app/code/functions/scale_up/scale_up.go @@ -2,6 +2,7 @@ package scale_up import ( "context" + "errors" "fmt" "net/http" "os" @@ -49,9 +50,9 @@ func Handler(w http.ResponseWriter, r *http.Request) { } scaleSetNames := common.GetScaleSetsNamesFromVmssState(ctx, subscriptionId, resourceGroupName, &vmssState) - scaleSets, err := common.GetScaleSetsList(ctx, subscriptionId, resourceGroupName, scaleSetNames) + scaleSetsByVersion, err := common.GetScaleSetsByVersion(ctx, subscriptionId, resourceGroupName, &vmssState) if err != nil { - logger.Error().Err(err).Msgf("cannot get scale sets list") + logger.Error().Err(err).Msgf("cannot get scale sets") common.WriteErrorResponse(w, err) return } @@ -64,16 +65,9 @@ func Handler(w http.ResponseWriter, r *http.Request) { return } - targetVersion, err := common.GetConfigHash(vmssConfig) - if err != nil { - logger.Error().Err(err).Msgf("cannot get vmss config hash") - common.WriteErrorResponse(w, err) - return - } - // 1. Initial VMSS creation flow: initiale vmss creation if needed if vmssState.IsEmpty() && !state.Clusterized { - err := createVmss(ctx, &vmssConfig, &vmssState, state.DesiredSize, targetVersion) + err := createVmss(ctx, &vmssConfig, &vmssState, state.DesiredSize) if err != nil { logger.Error().Err(err).Msgf("cannot create initial vmss") common.WriteErrorResponse(w, err) @@ -89,22 +83,28 @@ func Handler(w http.ResponseWriter, r *http.Request) { return } - if len(scaleSets) == 0 && !vmssState.IsEmpty() { + if len(scaleSetsByVersion) == 0 && !vmssState.IsEmpty() { err := fmt.Errorf("cannot find scale sets %v", scaleSetNames) logger.Error().Err(err).Send() common.WriteErrorResponse(w, err) return } - latestVmss := scaleSets[len(scaleSets)-1] + latestVmss, ok := scaleSetsByVersion[vmssState.GetLatestVersion()] + if !ok { + err := fmt.Errorf("cannot find latest vmss") + logger.Error().Err(err).Send() + common.WriteErrorResponse(w, err) + return + } currentConfig := common.GetVmssConfig(ctx, resourceGroupName, latestVmss) // 2. Update flow: compare current vmss config with expected vmss config and update if needed - if !targetVersionIsLatestVersion(vmssState.Versions, targetVersion) { + if !targetConfigIsLatestConfig(latestVmss, vmssConfig.ConfigHash) { diff := common.VmssConfigsDiff(*currentConfig, vmssConfig) logger.Info().Msgf("vmss config diff: %s", diff) - err := HandleVmssUpdate(ctx, currentConfig, &vmssConfig, &vmssState, state.DesiredSize, targetVersion) + err := HandleVmssUpdate(ctx, currentConfig, &vmssConfig, &vmssState, state.DesiredSize) if err != nil { common.WriteErrorResponse(w, err) return @@ -115,8 +115,8 @@ func Handler(w http.ResponseWriter, r *http.Request) { returnMsg := "" // 3. Refresh in progress flow: handle vmss refresh if needed - if len(scaleSets) > 1 && targetVersionIsLatestVersion(vmssState.Versions, targetVersion) { - err := progressVmssRefresh(ctx, scaleSets, &vmssState, state.DesiredSize) + if len(scaleSetsByVersion) > 1 && targetConfigIsLatestConfig(latestVmss, vmssConfig.ConfigHash) { + err := progressVmssRefresh(ctx, scaleSetsByVersion, &vmssState, state.DesiredSize) if err != nil { common.WriteErrorResponse(w, err) return @@ -134,15 +134,11 @@ func Handler(w http.ResponseWriter, r *http.Request) { common.WriteSuccessResponse(w, returnMsg) } -func HandleVmssUpdate(ctx context.Context, currentConfig, newConfig *common.VMSSConfig, vmssState *common.VMSSState, desiredSize int, newVersion string) error { +func HandleVmssUpdate(ctx context.Context, currentConfig, newConfig *common.VMSSConfig, vmssState *common.VMSSState, desiredSize int) error { logger := logging.LoggerFromCtx(ctx) - logger.Info().Msgf("updating vmss %s", currentConfig.Name) - // leaseId, err := common.LockContainer(ctx, stateStorageName, stateContainerName) - // if err != nil { - // return err - // } - // defer common.UnlockContainer(ctx, stateStorageName, stateContainerName, leaseId) + newConfigHash := newConfig.ConfigHash + logger.Info().Msgf("updating vmss %s to new config_hash %s", currentConfig.Name, newConfigHash) refreshNeeded := false if currentConfig.SKU != newConfig.SKU { @@ -150,78 +146,43 @@ func HandleVmssUpdate(ctx context.Context, currentConfig, newConfig *common.VMSS logger.Info().Msg(msg) refreshNeeded = true } else { - _, err := common.CreateOrUpdateVmss(ctx, subscriptionId, resourceGroupName, currentConfig.Name, newVersion, *newConfig, desiredSize) + _, err := common.CreateOrUpdateVmss(ctx, subscriptionId, resourceGroupName, currentConfig.Name, newConfigHash, *newConfig, desiredSize) if err != nil { - if updErr, ok := err.(*azcore.ResponseError); ok && updErr.ErrorCode == "PropertyChangeNotAllowed" { + var responseErr *azcore.ResponseError + if errors.As(err, &responseErr) && responseErr.ErrorCode == "PropertyChangeNotAllowed" { refreshNeeded = true } logger.Error().Err(err).Msgf("cannot update vmss %s", currentConfig.Name) - return err } else { - // replace active version in vmss state - vmssState.ReplaceVersion(currentConfig.Tags["version"], newVersion) - err = common.WriteVmssState(ctx, stateStorageName, stateContainerName, *vmssState) - if err != nil { - err = fmt.Errorf("cannot write vmss state: %w", err) - return err - } + logger.Info().Msgf("updated vmss %s to new config_hash %s", currentConfig.Name, newConfigHash) } } if refreshNeeded { - err := initiateVmssRefresh(ctx, newConfig, vmssState, newVersion, desiredSize) + err := createVmss(ctx, newConfig, vmssState, desiredSize) if err != nil { - logger.Error().Err(err).Msgf("cannot initiate vmss refresh") + logger.Error().Err(err).Msg("cannot create 'refresh' vmss") return err } logger.Info().Msgf("initiated vmss refresh") - return nil } - - logger.Info().Msgf("updated vmss %s", currentConfig.Name) return nil } -func initiateVmssRefresh(ctx context.Context, vmssConfig *common.VMSSConfig, vmssState *common.VMSSState, newVersion string, desiredSize int) error { - // Make sure that vmss current size is equal to "desired" number of weka instances - logger := logging.LoggerFromCtx(ctx) - logger.Info().Msgf("initiate vmss refresh") - - newVmssName := common.GetVmScaleSetName(prefix, clusterName, newVersion) - - // if public ip address is assigned to vmss, domainNameLabel should differ (avoid VMScaleSetDnsRecordsInUse error) - for i := range vmssConfig.PrimaryNIC.IPConfigurations { - newDnsLabelName := fmt.Sprintf("%s-v%s", vmssConfig.PrimaryNIC.IPConfigurations[i].PublicIPAddress.DomainNameLabel, newVersion) - vmssConfig.PrimaryNIC.IPConfigurations[i].PublicIPAddress.DomainNameLabel = newDnsLabelName - } - - // update hostname prefix - vmssConfig.ComputerNamePrefix = fmt.Sprintf("%s-v%s", vmssConfig.ComputerNamePrefix, newVersion) - - logger.Info().Msgf("creating new vmss %s of size %d", newVmssName, desiredSize) - - err := createVmss(ctx, vmssConfig, vmssState, desiredSize, newVersion) - if err != nil { - logger.Error().Err(err).Msgf("cannot create 'refresh' vmss %s", newVmssName) - return err - } - return nil -} - -func progressVmssRefresh(ctx context.Context, activeScaleSets []*armcompute.VirtualMachineScaleSet, vmssState *common.VMSSState, desiredSize int) error { +func progressVmssRefresh(ctx context.Context, scaleSetsByVersion map[int]*armcompute.VirtualMachineScaleSet, vmssState *common.VMSSState, desiredSize int) error { // Terminology: // "Outdated" vmss -- vmss that was used before refresh // "Refresh" vmss -- vmss that was created during refresh // "desired" number of weka instances -- number of weka instances expected by the user (stored in state) logger := logging.LoggerFromCtx(ctx) logger.Info().Msg("progressing vmss refresh") - logger.Info().Msgf("active scale sets number: %d", len(activeScaleSets)) + logger.Info().Msgf("active scale sets number: %d", len(scaleSetsByVersion)) - latestVmss := activeScaleSets[len(activeScaleSets)-1] - latestVmssSize := int(*latestVmss.SKU.Capacity) outdatedVmssTotalSize := 0 - - for _, vmss := range activeScaleSets[:len(activeScaleSets)-1] { + for version, vmss := range scaleSetsByVersion { + if version == vmssState.GetLatestVersion() { + continue + } size := int(*vmss.SKU.Capacity) outdatedVmssTotalSize += size @@ -233,9 +194,8 @@ func progressVmssRefresh(ctx context.Context, activeScaleSets []*armcompute.Virt logger.Error().Err(err).Send() return err } - // delete outdated hash from vmss state - version := vmss.Tags["version"] - err = vmssState.RemoveVersion(*version) + // delete outdated vmss version from state + err = vmssState.RemoveVersion(version) if err != nil { logger.Error().Err(err).Send() return err @@ -248,17 +208,32 @@ func progressVmssRefresh(ctx context.Context, activeScaleSets []*armcompute.Virt } } + latestVmss := scaleSetsByVersion[vmssState.GetLatestVersion()] + latestVmssSize := int(*latestVmss.SKU.Capacity) logger.Info().Msgf("refresh vmss (%s) size is %d, outdated vmss(es) total size is %d", *latestVmss.Name, latestVmssSize, outdatedVmssTotalSize) return nil } -func createVmss(ctx context.Context, vmssConfig *common.VMSSConfig, vmssState *common.VMSSState, vmssSize int, vmssVersion string) error { +func createVmss(ctx context.Context, vmssConfig *common.VMSSConfig, vmssState *common.VMSSState, vmssSize int) error { logger := logging.LoggerFromCtx(ctx) + vmssVersion := vmssState.DeduceNextVersion() + vmssConfigHash := vmssConfig.ConfigHash vmssName := common.GetVmScaleSetName(prefix, clusterName, vmssVersion) - logger.Info().Msgf("creating vmss %s", vmssName) - vmssId, err := common.CreateOrUpdateVmss(ctx, subscriptionId, resourceGroupName, vmssName, vmssVersion, *vmssConfig, vmssSize) + if vmssVersion > 0 { + // if public ip address is assigned to vmss, domainNameLabel should differ (avoid VMScaleSetDnsRecordsInUse error) + for i := range vmssConfig.PrimaryNIC.IPConfigurations { + newDnsLabelName := fmt.Sprintf("%s-v%d", vmssConfig.PrimaryNIC.IPConfigurations[i].PublicIPAddress.DomainNameLabel, vmssVersion) + vmssConfig.PrimaryNIC.IPConfigurations[i].PublicIPAddress.DomainNameLabel = newDnsLabelName + } + // update hostname prefix + vmssConfig.ComputerNamePrefix = fmt.Sprintf("%s-v%d", vmssConfig.ComputerNamePrefix, vmssVersion) + } + + logger.Info().Msgf("creating new vmss %s of size %d", vmssName, vmssSize) + + vmssId, err := common.CreateOrUpdateVmss(ctx, subscriptionId, resourceGroupName, vmssName, vmssConfigHash, *vmssConfig, vmssSize) if err != nil { return err } @@ -272,7 +247,8 @@ func createVmss(ctx context.Context, vmssConfig *common.VMSSConfig, vmssState *c err = common.AssignVmssContributorRoleToFunctionApp(ctx, subscriptionId, resourceGroupName, *vmssId, functionAppName) if err != nil { - if getErr, ok := err.(*azcore.ResponseError); ok && getErr.ErrorCode == "RoleAssignmentExists" { + var responseErr *azcore.ResponseError + if errors.As(err, &responseErr) && (responseErr.ErrorCode == "RoleAssignmentExists" || responseErr.RawResponse.StatusCode == 409) { logger.Info().Msgf("vmss %s 'contributor' role is already assigned to function app", vmssName) return nil } @@ -284,9 +260,7 @@ func createVmss(ctx context.Context, vmssConfig *common.VMSSConfig, vmssState *c return nil } -func targetVersionIsLatestVersion(activeVersions []string, targetVersion string) bool { - if len(activeVersions) == 0 { - return false - } - return activeVersions[len(activeVersions)-1] == targetVersion +func targetConfigIsLatestConfig(latestScaleSet *armcompute.VirtualMachineScaleSet, targetVersion string) bool { + configHash := latestScaleSet.Tags["config_hash"] + return configHash != nil && *configHash == targetVersion } diff --git a/variables.tf b/variables.tf index c965a01f..3199f4e9 100644 --- a/variables.tf +++ b/variables.tf @@ -321,7 +321,7 @@ variable "function_app_storage_account_container_prefix" { variable "function_app_version" { type = string description = "Function app code version (hash)" - default = "9611fb1feebc0c4e3f8b34671fa909fe" + default = "abe83f3f0224c29630262452d616dffd" } variable "function_app_dist" {