Skip to content

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

☁️ Azure SQL Managed Instance Start/Stop Schedule Terraform Module

A weekly schedule of running windows for a SQL Managed Instance, so it is billed only while it is needed — targeting hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Blast radius


🧩 Overview

  • ⏰ Records the weekly windows in which a SQL Managed Instance runs — everything outside them is downtime.
  • 🧮 Computes and emits the running and stopped hours per week, because that number is the decision and nobody derives it reliably by hand.
  • ⌛ Enforces the HH:MM format the provider does not check at all.
  • ⚠️ States the fact that catches everyone: the times are when the operation is initiated, not when the instance is ready.
  • 🔕 Reports that a stop can be silently skipped, so a saving forecast is an upper bound.

💡 Why it matters: the schedule is not a set of maintenance events layered onto a running instance — it is the instance's uptime. A single Monday-to-Friday window means every database is unreachable all weekend. Applying this to an instance that currently runs continuously will take it offline at the first gap, which is the feature working correctly and is rarely what a reader expects on first reading.


❤️ Support this project

If this module saved you time:


🗺️ Where this fits in the family

flowchart TB
    MI["terraform-azurerm-mssql-managed-instance"]
    THIS["terraform-azurerm-mssql-managed-instance-start-stop-schedule"]
    FOG["terraform-azurerm-mssql-managed-instance-failover-group"]
    ADM["terraform-azurerm-mssql-managed-instance-active-directory-administrator"]
    TDE["terraform-azurerm-mssql-managed-instance-transparent-data-encryption"]
    SRV["terraform-azurerm-mssql-server"]

    MI -->|"id"| THIS
    MI -->|"id"| FOG
    MI -->|"id"| ADM
    MI -->|"id"| TDE
    THIS -.->|"a stopped instance serves no failover role"| FOG
    SRV -.->|"logical servers cannot be stopped this way"| THIS

    style THIS fill:#0078D4,stroke:#004578,color:#ffffff
    style MI fill:#004578,stroke:#004578,color:#ffffff
    style FOG fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style ADM fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style TDE fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style SRV fill:#F3F2F1,stroke:#8A8886,color:#201F1E
Loading

Every module in this family takes the same managed instance's id; this one differs in what it does with it. The dotted edge to the failover group is the interaction worth thinking about before combining them — a stopped instance is not serving any failover role, so a schedule and a disaster-recovery pairing on the same instance are two controls with opposite assumptions. The other dotted edge is a reminder that a logical SQL server cannot be stopped this way at all.


🧬 What this module builds

flowchart TB
    MID["managed_instance_id (required, force-new)"]
    SCH["schedule (required, at least one running window)"]
    TZ["timezone_id (Windows zone, defaults to UTC)"]

    SS["azurerm_mssql_managed_instance_start_stop_schedule.this"]

    OID["id"]
    ORUN["running_hours_per_week"]
    OSTOP["stopped_hours_per_week"]
    ONEXT["next_execution_time"]

    MID --> SS
    SCH --> SS
    TZ --> SS
    SS --> OID
    SS --> ORUN
    SS --> OSTOP
    SS --> ONEXT

    style SS fill:#0078D4,stroke:#004578,color:#ffffff
    style SCH fill:#004578,stroke:#004578,color:#ffffff
    style MID fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style TZ fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style OID fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style ORUN fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style OSTOP fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style ONEXT fill:#F3F2F1,stroke:#8A8886,color:#201F1E
Loading
Resource Count Notes
azurerm_mssql_managed_instance_start_stop_schedule.this 1 One per instance; the Resource ID always ends /startStopSchedules/default

✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None in this module — the caller configures the provider, its auth, and the mandatory features {} block

Schema notes that bite:

  • 🔴 The times are when the operation is INITIATED, not when the instance is ready. Microsoft's worked example: to be available at 8 AM, schedule the start at 7:40 AM.
  • 🔴 The provider applies no time-format check. start_time and stop_time carry only a non-empty check, so 9am, 09:00:00 and 25:00 all pass its validation and fail at Azure. The HH:MM rule here is the module's.
  • 🔴 Microsoft documents two rules the provider does not enforce: scheduled pairs may not overlap, and any two successive actions must be at least one hour apart. The API rejects violations at apply. This module refuses the unambiguous cases and reports the rest.
  • 🔴 A stop can be silently skipped. If a conflicting operation is in progress, Azure retries after ten minutes and then abandons that occurrence — the instance keeps running and nothing reports it.
  • 🔴 General Purpose tier only. A Business Critical instance cannot be stopped, and this module receives only a Resource ID.
  • ⚠️ schedule carries a minimum of one item, invisible in the binary schema — an empty list fails with "Not enough list items".
  • ⚠️ The day names are a closed, CASE-SENSITIVE set: monday and MONDAY are both refused.
  • ⚠️ timezone_id is a WINDOWS identifier, not IANA — Central Europe Standard Time, not Europe/Berlin. The provider checks only non-emptiness.
  • ℹ️ There IS a requires-import guard on create, so a clean apply is evidence no schedule existed.
  • ℹ️ Delete removes the schedule and leaves the instance where it is — including stopped, with nothing left to start it.
  • ℹ️ No tags, no location. All four timeouts are honoured (30/5/30/30) and govern writing the schedule, not running it.

🔑 Required Azure RBAC Roles / Permissions

Permission Scope Why
Microsoft.Sql/managedInstances/startStopSchedules/read and /write the managed instance create, update and delete the schedule
SQL Managed Instance Contributor (or Contributor) the managed instance the built-in role carrying the above

🔴 Whoever can write this resource can take every database on the instance offline, on a recurring basis, by editing four strings. It is a smaller-looking permission than it is.

ℹ️ No data-plane permission is involved. The schedule is control-plane metadata and says nothing about who may read the databases while they are up.


Azure Prerequisites

  • The Microsoft.Sql resource provider registered on the subscription.
  • An existing SQL Managed Instance in the General Purpose service tier. Business Critical instances cannot be stopped.
  • A decision about the Windows time zone — UTC is the default and the only one that does not shift twice a year.
  • The caller configures provider "azurerm" { features {} }, auth and subscription.

📁 Module Structure

terraform-azurerm-mssql-managed-instance-start-stop-schedule/
├── providers.tf    # required_version + pinned azurerm; no provider block
├── variables.tf    # 5 inputs, deeply typed, with the format and timing rules
├── main.tf         # the single keystone `this`
├── outputs.tf      # id first, then the computed weekly hours, then the posture facts
├── README.md       # this file
├── SCOPE.md        # the cross-module contract
├── LICENSE         # MIT
└── .gitignore

⚙️ Quick Start

provider "azurerm" {
  features {}
}

module "sqlmi_schedule" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-start-stop-schedule.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi_dev.id

  schedule = [
    {
      start_day  = "Monday"
      start_time = "07:40"
      stop_day   = "Friday"
      stop_time  = "19:00"
    },
  ]
}

That instance is now stopped from Friday evening until Monday morning — about 60 hours a week. Read example 1 before applying it to anything that currently runs continuously.


🔌 Cross-Module Contract

Consumes

Input Type Source
managed_instance_id Resource ID terraform-azurerm-mssql-managed-instance → id
schedule list(object) the caller
timezone_id Windows zone name the caller

Emits

Output Description
id Resource ID of the schedule (first)
running_hours_per_week / stopped_hours_per_week the numbers the decision turns on
running_windows the windows as readable strings
next_execution_time / next_run_action the service's own interpretation
this_schedule_never_stops_the_instance true when the windows cover the week

📚 Example Library

1 · A working-week schedule, and what it costs in availability
module "sqlmi_schedule" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-start-stop-schedule.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi_dev.id

  schedule = [
    {
      start_day  = "Monday"
      start_time = "07:40"
      stop_day   = "Friday"
      stop_time  = "19:00"
    },
  ]
}

output "uptime" {
  value = {
    running = module.sqlmi_schedule.running_hours_per_week # 107.33
    stopped = module.sqlmi_schedule.stopped_hours_per_week # 60.66
  }
}

🔴 Every hour outside a window is downtime. This instance is unreachable all weekend — that is the feature, and it is why the module computes the hours rather than leaving them to be inferred from four strings.

⚠️ Applying this to an instance that currently runs continuously takes it offline at the first gap.

2 · The forty-minute head start
schedule = [
  {
    start_day  = "Monday"
    start_time = "07:40" # so the instance is UP by 08:00
    stop_day   = "Friday"
    stop_time  = "19:00"
  },
]

🔴 Scheduled items are when the start and stop actions are INITIATED, not when the instance is up and running. That is Microsoft's own wording, and their worked example is exactly this: schedule the start at 7:40 AM if you want the instance available at 8 AM.

⚠️ A schedule written as though the times were availability windows will have the instance still starting when the working day begins — and nothing in Terraform or Azure will flag it.

3 · Several windows in one week
module "sqlmi_schedule" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-start-stop-schedule.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi_dev.id

  schedule = [
    {
      start_day  = "Monday"
      start_time = "07:40"
      stop_day   = "Friday"
      stop_time  = "19:00"
    },
    {
      start_day  = "Saturday"
      start_time = "08:00"
      stop_day   = "Saturday"
      stop_time  = "12:00"
    },
  ]
}

output "windows" {
  # ["Monday 07:40 -> Friday 19:00", "Saturday 08:00 -> Saturday 12:00"]
  value = module.sqlmi_schedule.running_windows
}

ℹ️ The windows render in the order given. This module does not sort them — sorting would produce a plan diff when only the author's arrangement had changed.

⚠️ Microsoft forbids overlapping pairs and the API rejects them. This module does not attempt overlap detection, because it would need a week ordering the service does not publish; hours summing past 168 is the hint, and this_schedule_never_stops_the_instance catches the extreme case.

4 · The format the provider does not check
# ALL REFUSED at plan by this module -- and ALL accepted by the provider:
#   start_time = "9am"
#   start_time = "09:00:00"
#   start_time = "25:00"
#   start_time = "09:60"
#
# Correct:
schedule = [
  { start_day = "Monday", start_time = "09:00", stop_day = "Monday", stop_time = "17:00" },
]

🔴 start_time and stop_time carry only a non-empty check in the provider, so every one of those values passes terraform validate on a bare resource and fails at Azure. The HH:MM rule is the module's, taken from Microsoft's own examples.

ℹ️ The day names are checked by the provider, against a closed case-sensitive set — monday is refused there too.

5 · The one-hour rule, where it can be judged
# REFUSED -- same day, 30 minutes apart:
#   { start_day = "Monday", start_time = "09:00", stop_day = "Monday", stop_time = "09:30" }
#
# ACCEPTED -- exactly one hour:
schedule = [
  { start_day = "Monday", start_time = "09:00", stop_day = "Monday", stop_time = "10:00" },
]

⚠️ Microsoft requires at least one hour between any two successive start and stop actions, and the API rejects a shorter gap.

💡 This module refuses only the unambiguous same-day case. A window whose stop falls earlier than its start may be crossing the week boundary, and judging it would need a day ordering the service does not publish — so those are listed in cross_day_windows_are_not_checked_against_the_one_hour_rule rather than guessed at.

6 · Time zones, and the one that moves
module "sqlmi_schedule" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-start-stop-schedule.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi_dev.id
  timezone_id         = "Eastern Standard Time" # a WINDOWS identifier

  schedule = [
    { start_day = "Monday", start_time = "07:40", stop_day = "Friday", stop_time = "19:00" },
  ]
}

🔴 Europe/Berlin and America/New_York are IANA names and are refused here — Azure wants Windows identifiers. That is the one mistake this module catches; any other string is passed through, because the full Windows list is not published by the provider.

⚠️ A zone observing daylight saving moves the schedule twice a year relative to UTC. UTC is the default because it does not — which makes it predictable and makes it the wrong choice if the point is to match an office's working day. Decide which you want.

7 · Checking what the service thinks the schedule means
output "service_interpretation" {
  value = {
    next_at     = module.sqlmi_schedule.next_execution_time
    next_action = module.sqlmi_schedule.next_run_action
  }
}

💡 These are read back from Azure, not computed here. They are the honest way to confirm a schedule means what its author intended — particularly across a time-zone choice or a window that crosses midnight, where the module's own hour arithmetic rests on a stated assumption.

8 · A schedule that saves nothing
output "pointless" {
  value = module.sqlmi_schedule.this_schedule_never_stops_the_instance
}

⚠️ A schedule whose windows cover the whole week is legal and expensive. It implies a cost control while providing none, and it is easy to arrive at by adding windows one at a time. The flag catches it before someone reports a saving that never existed.

9 · Why the saving is an upper bound
output "forecast_caveat" {
  value = {
    stopped_hours = module.sqlmi_schedule.stopped_hours_per_week
    may_be_skipped = module.sqlmi_schedule.a_stop_can_be_silently_skipped
  }
}

🔴 A stop can be silently skipped. Microsoft documents that if a conflicting operation — a vCore scaling, for instance — is in progress when a stop is triggered, the mechanism retries after ten minutes and then abandons that occurrence. The instance keeps running, and nothing in Terraform reports it.

💡 So stopped_hours_per_week is the maximum saving, not the expected one.

10 · Saying why, in the only field that can
module "sqlmi_schedule" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-start-stop-schedule.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi_dev.id
  description         = "Dev instance: deliberately off nights and weekends. Do not extend without asking the data team."

  schedule = [
    { start_day = "Monday", start_time = "07:40", stop_day = "Friday", stop_time = "19:00" },
  ]
}

💡 A schedule is a set of times with no explanation attached. The next person to read it will want to know whether the weekend gap is deliberate cost saving or an oversight — and description is the only place in the resource that can answer.

11 · A schedule and a failover group on the same instance
module "sqlmi_schedule" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-start-stop-schedule.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi_dev.id # a DEV instance, not the DR secondary

  schedule = [
    { start_day = "Monday", start_time = "07:40", stop_day = "Friday", stop_time = "19:00" },
  ]
}

⚠️ Think carefully before scheduling an instance that is part of a failover group. A stopped instance is not serving any disaster-recovery role, so a schedule on a DR secondary quietly removes the protection the failover group exists to provide — for most of the week.

💡 Schedules belong on development and test instances. That is the case this module is built around.

12 · Several instances from one map
module "sqlmi_schedule" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-start-stop-schedule.git?ref=v1.0.0"
  for_each = module.dev_instances

  managed_instance_id = each.value.id
  description         = "Non-production: off outside working hours"

  schedule = [
    { start_day = "Monday", start_time = "07:40", stop_day = "Friday", stop_time = "19:00" },
  ]
}

output "weekly_stopped_hours" {
  value = { for k, m in module.sqlmi_schedule : k => m.stopped_hours_per_week }
}

output "any_saving_nothing" {
  value = [for k, m in module.sqlmi_schedule : k if m.this_schedule_never_stops_the_instance]
}

💡 Keying by a stable name means adding or removing an instance never re-indexes the rest, and the first output turns a fleet-wide cost question into one map.

13 · 🏗️ End-to-end composition
provider "azurerm" {
  features {}
}

data "azurerm_client_config" "current" {}

module "sqlmi_dev" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"

  name                = "sqlmi-dev-eus2"
  resource_group_name = "rg-data-dev"
  location            = "eastus2"

  # General Purpose -- stop and start is not available on Business Critical.
  sku_name           = "GP_Gen5"
  vcores             = 4
  storage_size_in_gb = 256
  subnet_id          = var.dev_subnet_id
  license_type       = "BasePrice"
  # Both are required through this module: the provider pairs each with an
  # inline Entra administrator block that this module does not expose.
  administrator_login          = "sqladmin"
  administrator_login_password = var.mi_admin_password # out of band; never committed
}

module "sqlmi_dev_entra_admin" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-active-directory-administrator.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi_dev.id
  login_username      = "sql-mi-admins"
  object_id           = var.sql_admin_group_object_id
  tenant_id           = data.azurerm_client_config.current.tenant_id
}

module "sqlmi_schedule" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-start-stop-schedule.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi_dev.id
  timezone_id         = "UTC"
  description         = "Dev instance: off nights and weekends to control cost"

  schedule = [
    {
      start_day  = "Monday"
      start_time = "07:40" # up by 08:00
      stop_day   = "Friday"
      stop_time  = "19:00"
    },
  ]
}

output "dev_instance_cost_posture" {
  value = {
    running_hours   = module.sqlmi_schedule.running_hours_per_week
    stopped_hours   = module.sqlmi_schedule.stopped_hours_per_week
    fraction_up     = module.sqlmi_schedule.running_fraction_of_week
    windows         = module.sqlmi_schedule.running_windows
    next_action_at  = module.sqlmi_schedule.next_execution_time
    saving_is_a_max = module.sqlmi_schedule.a_stop_can_be_silently_skipped
  }
}

💡 A General Purpose development instance is the case this module is built around — the tier is stated in the composition because Business Critical instances cannot be stopped at all.

⚠️ The Entra administrator is configured independently of the schedule, and it keeps working across stops and starts. Nothing about a stopped instance changes who administers it.

🔴 The instance is unreachable roughly 60 hours a week under this schedule. That is the intent; make sure everything that runs against it — backups, agent jobs, CI pipelines — expects it.


📥 Inputs

Identity — managed_instance_id (required, force-new). Windows — schedule (required, at least one). Interpretation — timezone_id, description. Tail — timeouts. (There is no tags: the resource exposes none.)

Full schemas
Name Type Default Notes
managed_instance_id string — Required, force-new. Anchored; a logical server ID is refused.
schedule list(object({start_day, start_time, stop_day, stop_time})) — Required, min 1. Days case-sensitive; times HH:MM, enforced by the module.
timezone_id string "UTC" A Windows zone name. IANA names refused.
description string null The only place to record why.
timeouts object({create, read, update, delete}) null All four real; they govern writing the schedule, not running it.

🧾 Outputs

Output Description
id Resource ID of the schedule (first)
managed_instance_id / managed_instance_name / resource_group_name / subscription_id the parent
timezone_id as configured
running_windows the windows as readable strings
schedule_item_count how many windows
running_hours_per_week / stopped_hours_per_week the computed weekly figures
running_fraction_of_week a proxy for the compute saving
next_execution_time / next_run_action the service's own interpretation
creating_this_resource_stops_a_database constant true
the_times_are_when_the_operation_starts_not_when_it_finishes constant true
this_schedule_never_stops_the_instance true when the windows cover the week
cross_day_windows_are_not_checked_against_the_one_hour_rule what the module could not judge
overlapping_windows_are_rejected_by_azure_not_by_this_module constant true
the_provider_applies_no_time_format_check constant true
a_stop_can_be_silently_skipped constant true
this_feature_is_general_purpose_tier_only constant true
stopping_is_a_billing_control_not_a_restart constant true
the_time_zone_applies_to_every_window constant true
a_daylight_saving_zone_moves_the_schedule_twice_a_year constant true
an_existing_schedule_blocks_creation constant true
destroying_this_resource_leaves_the_instance_running constant true
one_schedule_per_managed_instance constant true
force_new_fields / fields_that_can_change_after_creation lifecycle
fields_azure_returns_on_read where drift is detectable
all_four_timeouts_are_honoured_here constant true
this_resource_supports_no_azure_resource_tags constant true

🔒 No secret is accepted or emitted: this resource records times.


🧠 Architecture Notes

The windows are the uptime. That is the single idea this module is built around. A schedule entry is not a maintenance event layered onto a running instance — the instance runs between the start and the stop and is stopped the rest of the time. So the module computes running_hours_per_week and stopped_hours_per_week and puts them in front of the reader, because the hours are the decision and a list of four-field objects is not a good way to see them.

Those figures rest on a stated assumption. Week-minutes are computed with Monday as the first day, and a window whose stop falls at or before its start is treated as crossing the week boundary. The service does not publish an ordering, so the output descriptions say so and point at next_execution_time — which is Azure's own answer — as the way to check.

Two rules are enforced that the provider does not check at all. The HH:MM format comes from Microsoft's examples, not from a provider validator, and the error message says so. Microsoft's one-hour minimum between successive actions is enforced only for unambiguous same-day windows, because judging a window that may wrap the week would require the ordering the service does not publish. The rest are listed in an output rather than guessed at — the module says what it could not check.

Overlap is left to Azure. Microsoft forbids overlapping pairs and the API rejects them, but reliable detection needs the same unpublished ordering. running_hours_per_week summing past 168 is the practical hint, and this_schedule_never_stops_the_instance catches the extreme case.

The saving is an upper bound, not a forecast. A stop is skipped entirely if a conflicting operation is still running ten minutes later, and nothing reports that. Any cost model built on stopped_hours_per_week should be read as the best case.

Nothing is inverted here. timezone_id keeps the provider's UTC, which is the predictable choice; and there is no default schedule, because there is no safe one — a schedule is by definition downtime, so it must be typed.

Destroy is the reassuring direction. Deleting the schedule removes the schedule and nothing else: the instance stays in whatever state it is in. A destroy performed while the instance is stopped leaves it stopped, with nothing scheduled to start it again — worth knowing before doing it on a Friday.


🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller must type it)
The schedule itself no default — downtime is always explicit —
Time zone UTC — the only zone that does not shift twice a year a Windows zone name
Time format HH:MM enforced, though the provider checks nothing —
One-hour rule enforced where unambiguous, reported where it is not —
Overlap left to Azure, and said so —
Window ordering preserved as written, never sorted —
Secret handling none — this resource records times not available

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin the module with ?ref=v1.0.0 — never a branch. This module is plan-only in this repository; a human applies from CI.


🧪 Testing

terraform validate proves, offline and with no credentials: the anchored managed_instance_id pattern, the at-least-one-window rule, the closed case-sensitive day set, the HH:MM format on both times, the zero-length-window refusal, the same-day one-hour minimum, the IANA-time-zone refusal, and the whitespace-only description refusal.

Only terraform plan and apply exercise: whether the instance exists, whether it is General Purpose, and whether Azure accepts the windows as non-overlapping.

Nothing at either stage tells you that a stop was skipped, or that the instance is down when someone needs it. That is what next_execution_time, the computed hours and the constant outputs are for.

Note the asymmetry: a validation failure blocks terraform destroy as well as apply, which is why the timing checks are narrowed to the cases that cannot be legal rather than applied to every window.


💬 Example Output

id                       = "/subscriptions/.../resourceGroups/rg-data-dev/providers/Microsoft.Sql/managedInstances/sqlmi-dev-eus2/startStopSchedules/default"
managed_instance_name    = "sqlmi-dev-eus2"
timezone_id              = "UTC"
running_windows          = ["Monday 07:40 -> Friday 19:00"]
schedule_item_count      = 1
running_hours_per_week   = 107.33
stopped_hours_per_week   = 60.66
running_fraction_of_week = 0.6388888888888888
next_execution_time      = "2026-08-17T07:40:00Z"
next_run_action          = "Start"
force_new_fields         = ["managed_instance_id"]

🔍 Troubleshooting

Symptom Cause Fix
a schedule entry has a start_time or stop_time that is not HH:MM 9am, 09:00:00, 25:00 or similar Use 24-hour HH:MM. The provider accepts these and fails at Azure; this check is the module's
a schedule entry has a start_day or stop_day outside the closed set wrong case or an abbreviation Monday, not monday or Mon
schedule must contain at least one window an empty list The provider requires at least one item
a schedule entry starts and stops at the same moment identical day and time Give the instance a period to run in
a schedule entry starts and stops on the same day less than an hour apart under Microsoft's one-hour minimum Widen the window
timezone_id looks like an IANA time zone Europe/Berlin or similar Use the Windows name — W. Europe Standard Time
Azure rejects the schedule as overlapping Microsoft forbids overlapping pairs; this module does not detect it Check running_windows and whether the hours sum past 168
The instance was still starting when the working day began the times are when the operation is initiated Move the start earlier — Microsoft's example is 20 minutes
The instance did not stop when it should have a conflicting operation was in progress; Azure retried once and skipped it Expected. Nothing reports it, and the saving is an upper bound
Apply fails and the instance is Business Critical stop and start is a General Purpose capability This module cannot see the tier; the failure appears at apply
The schedule ran an hour early or late after a clock change the time zone observes daylight saving Expected. Use UTC if predictability matters more than local working hours
The schedule was removed and the instance stayed stopped delete removes the schedule, not the instance state Start the instance manually; nothing is left to start it
A requires-import error on first apply a schedule already exists on the instance Expected, and better than overwriting. terraform import it

🔗 Related Docs


💙 "Infrastructure as Code should be standardized, consistent, and secure."