As I cover extensively in my book, Mastering Terraform, one of Terraform's superpowers is its extensibility. This extensibility allows it to essentially act as a one-stop-shop for controlling any platform's control plane with the same familiar language, command line tool and core workflow.

Each provider is its own [semi-]independent piece of software with its own codebase, maintainers, dependencies and design patterns. A provider's main purpose in life is to provide resources; resources that Terraform can create and manage. These resources model real entities on whatever platform the provider has taken on the responsibility in automating. On Azure, most of these resources are Azure Services. Sometimes these resources are deprecated. This can happen when there is a fundamental design change within the way the Terraform resource models the corresponding real-world entity — or it can happen when a service is retired (which happens more than you'd think).

When there is a fundamental design change big enough to create a completely new resource for the same entity, the maintainers of the provider mark the old resource as deprecated and publish a new resource. This allows existing users of the old resource to be put on notice that they need to migrate to the new resource type and that any new projects should use the new resource.

This happened most notably in the "azurerm" resources along a Windows / Linux split. Many resources for Virtual Machines, Virtual Machine Scale Sets, and the like were split into two resources: one for Windows and the other for Linux.

This design change affected the Azure App Service as well so today I thought I would take a closer look at the problem of migrating off deprecated resources in Terraform. We'll start with a hypothetical codebase set in version 2.99.0 of the azurerm Terraform provider.

I say hypothetical because this version of the Terraform provider was released over three years ago! Surely, every responsible IT professional has upgraded their Terraform provider at least once between now and then. Surely… :o)

None
Azure Services mapped to "azurerm" Terraform provider resources

The problem statement is this:

You're a team managing your infrastructure using Terraform for a long time. You originally deployed an application over three (3) years ago using Terraform and the "azurerm" provider version 2.99.0. Using this version of the provider, you provisioned an Azure App Service Plan and an Azure App Service running your .NET Framework Web Application. Now fasterforward to August 22, 2024 and the good folks at HashiCorp have released a new version of the "azurerm" provider: version 4.0.0 and you think you've delayed it too long, it's time to upgrade.

So what do you do?

1. Pin your "azurerm" provider version to 2.99

First, if you haven't done so already. You should absolutely lock your Terraform configuration to the old version of the provider while you take time to think and plan your upgrade path.

Your required providers block probably looks like this:

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 2.99.0"
    }
  }
}

The "~>" is called the pessemistic constraint. It will only allow your provider to be upgraded to a patch release like 2.99.1 or 2.99.2 but will not allow an upgrade to 2.100.0 for example.

In order to pin it to a specific version you simply change the "~>" to a "=". Like this:

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 2.99.0"
    }
  }
}

2. Upgrade to "azurerm" 3.x

Ultimately we want to get to azurerm 4.0 but before we get there we need to take baby steps. First we should upgrade to 3.x and then upgrade to 4.0. This will give us more stability and allow us to bring in a smaller amount of change and respond to that change, possibly mitigating any issues that might arise before jumping whole hog into 4.0.

SPOILER ALERT: Because we are using deprecated resources, skipping this step would be a non-starter and could be catastrophic to the stability of our Terraform workspace.

When we attempt this upgrade we should do it in a non-production environment that we don't care a lot about. This could be an environment that we provision specifically for the purpose of testing the upgrade path or an existing development environment that we aren't worried about completely borking.

We should also target a version of 3.x that will be easy for us to upgrade to 4.x. Usually there are what are called "bridge versions" that are specific versions of the Terraform provider that have been well tested against the upgrade path to the next major release. In the case of azurerm 4.x, versions 3.114.0 and 3.116.0 are both bridge versions that should provide a stable upgrade path.

After we've selected the workspace we want to use we can simply change the version to "~> 3.116.0".

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.116.0"
    }
  }
}

After we run terraform plan or apply again we will get a new warning for the Azure App Service Plan and the Azure App Service App resources.

The `azurerm_app_service_plan` resource has been deprecated in version 3.0 of the AzureRM provider and will be removed in version 4.0. Please use `azurerm_service_plan` resource instead.

You can see we are clearly notified that azurerm_app_service_plan was used to provision an App Service Plan. Likewise we'll see a similar warning for the Azure App Service App resource azurerm_app_service.

The `azurerm_app_service` resource has been deprecated in version 3.0 of the AzureRM provider and will be removed in version 4.0. Please use `azurerm_linux_web_app` and `azurerm_windows_web_app` resources instead.

Both of these warnings clearly articulate potential replacement resources that we should use to avoid running into future problems in the next major release 4.0.

So what happens if somebody has provisioned an App Service Plan and an App Service using azurerm version 2.99 and then wants to upgrade to version 4.0? We need to change the resource types from the old ones to the new ones. Luckily these potential replacements are available long, long before they are ultimately removed in 4.0.

Below we can see the upgrade path from the resources we are using in version 2.99.0 and 3.x.

None
Upgrade path for deprecated resource types

First, while they investigate the impact of the upgrade they need to change the required version from ~> 2.99 to = 2.99.0. This will ensure there is no automatic upgrade.

First step is to upgrade to an upgrade bridge version of the provider. To go from 2.99 to 4.0 we should first upgrade to 3.x. In order to upgrade to 3.x we should upgrade to 3.116.0 as this version is the upgrade bridge version for the 3.x version of the provider to get to 4.0.

If you just try and run terraform init you will get this error message:

None
Changing provider versions without telling anybody is a bad idea…

Changing the Terraform Provider Version requires running Terraform Init with the Upgrade Flag

So we need to run terraform init -upgrade.

None
Don't forget the -upgrade command option!!!

Terraform Init with the Upgrade Flag

After that we see that we have successfully initialized with the new version of the provider.

Change the resource types

Now that we are running version 3.116.0 of the azurerm provider we can continue to manage our resources. However, when we run terraform plan, we now get a warning message about deprecated resources.

│ Warning: Deprecated Resource
│ 
│   with azurerm_app_service_plan.main,
│   on main.tf line 14, in resource "azurerm_app_service_plan" "main":
│   14: resource "azurerm_app_service_plan" "main" {
│ 
│ The `azurerm_app_service_plan` resource has been superseded by the `azurerm_service_plan` resource. Whilst this resource will continue to be available in the 2.x
│ and 3.x releases it is feature-frozen for compatibility purposes, will no longer receive any updates and will be removed in a future major release of the Azure
│ Provider.
│ 
│ (and 3 more similar warnings elsewhere)

This is just telling us that it's okay we are still managing resources for the App Service Plan using the deprecated resource type of azurerm_app_service_plan but these resource types will be removed in the 4.x version of the provider.

So we should upgrade this resource to the new type azurerm_service_plan.

This new resource has some new required attributes that we didn't specify in our previous configuration when we used the old azurerm_app_service_plan.

  • os_type
  • sku_name

The os_type is a bit weird because we don't have anything related to this in our previous configuration. That could be that the attribute just didn't exist on the previous resource at all or we are using a default value without even knowing it. In this case, it is the latter. The os_type seems to have replaced an old attribute called kind which essentially has Operating System type as valid values. The default for kind was Windows so that's probably what we had originally and since changing this attribute requires the resource to be re-created we definitely don't want to accidentally change that.

It appears that the old sku nested block has been replaced with a single string attribute. Luckily it's not too difficult to find our SKU since its already in the valid values list so we just need to replace the example's value of P1v2 with S1.

Refactor with the moved block

Terraform has the ability to refactor resources by moving them from one place to another. This can be doing using either a declarative approach or an imperative approach.

First, let's declare a new resource with the new resource type.

resource "azurerm_service_plan" "main" {
  name                = "asp-ep112-${random_string.suffix.result}"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  os_type             = "Windows"
  sku_name            = "S1"
}

Then let's declare a moved block in order to move our resource from the old type to the new type:

moved {
  from = azurerm_app_service_plan.main
  to   = azurerm_service_plan.main
}

You would think this would be the right thing to do but unfortunately it doesn't work. The moved block is not intended for transforming a deprecated type into a new type. It is intended specifically for module refactoring, that is to move the same type resources from one module to another module.

│ Error: Unsupported `moved` across resource types
│ 
│   on main.tf line 33:
│   33: moved {
│ 
│ The provider "registry.terraform.io/hashicorp/azurerm" does not support moved operations across resource types and providers.

So that's not going to work. It seems the only way to get this to work is to remove and import.

Refactor with the terraform state mv command

Maybe we can try the imperative command to achieve the same thing? Alas, no.

terraform state mv azurerm_app_service_plan.main azurerm_service_plan.main

You get a similar error message:

Error: Invalid state move request
│ 
│ Cannot move azurerm_app_service_plan.main to azurerm_service_plan.main: resource types don't match.

I highlight this because one might think that they need to do a move operation. Afterall, we don't want to delete our existing resources, and it might be easy to think about the operation we are trying to perform as moving from one resource type to another. However, this use case is not supported by the moved block or the move command using the CLI. So, what other options do we have?

Remove and import

Really our only recourse is to remove the resources and re-import them. I know what you must be thinking?! Remove them!? That will cause an outage and this is unacceptable! But rest assured — when I talk about removing them I only mean from Terraform State. Your App Service and it's App Service Plan will continue running without a hiccup. We are simply telling Terraform to forget about them and then remember them under a new resource type (the ones that aren't deprecated).

Before we do that, we want to get the Azure Resource IDs for each of these resources.

First, we open Terraform Console by executing the terraform console command.

None

Use Terraform Console to access the Azure Resource ID for the App Service Plan

Then we can just request the resource ID of the App Service Plan by typing in the fully qualified resource path and the attribute we want it to output.

azurerm_app_service_plan.main.id

This will then respond with the value stored in the id attribute of the azurerm_app_service_plan.main resource.

/subscriptions/32cfe0af-c5cf-4a55-9d85-897b85a8f07c/resourceGroups/rg-ep112-5w97hr/providers/Microsoft.Web/serverfarms/asp-ep112-5w97hr

This will be useful in our import block in the next step. So, let's open up notepad and keep them safe for the time being.

Let's do the same for the App Service itself.

None

Use Terraform Console to access the Azure Resource ID for the App Service

Again, using the console command, we can access any attribute from any resource in our module.

azurerm_app_service.main.id

This yields the following Azure Resource ID. Again, very important for later.

/subscriptions/32cfe0af-c5cf-4a55-9d85-897b85a8f07c/resourceGroups/rg-ep112-5w97hr/providers/Microsoft.Web/sites/app-ep112-5w97hr

Now we can remove these resources from Terraform State using the removed block.

removed {
  from = azurerm_app_service_plan.main
  lifecycle {
    destroy = false
  }
}

If we attempt to run Terraform plan again we will encounter errors. That's because we need to remove the resource from code that is being referenced by the removed block. Under normal conditions, doing so would trigger the resource to be deleted from Azure. However, the removed block simply prevents this resource from being destroyed. Which is what we want.

Follow the right sequence: Remove the App Service 'App'

In order to do this, we have to follow a certain sequence. That's because if we remove the App Service Plan without removing the dependent resources we will run into trouble. Remember, the App Service's directly reference the App Service Plan! If these app services were in some other Terraform root module and used a data source we wouldn't have any problem but because they are in the same root module the App Service references the App Service Plan using the resource block that created it.

So first, we need to remove the App Service. This will remove all blocks that draw dependencies on the App Service Plan, which will allow us to remove it.

We can just temporarily comment out the azurerm_app_service resource block because we will need to refactor it in order to bring it back using and import block.

We get the following warning:

│ Warning: Some objects will no longer be managed by Terraform
│ 
│ If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them:
│  - azurerm_app_service.main
│ 
│ After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.

We see proof of the resources that will be removed but not destroyed in the terraform plan.

Terraform will perform the following actions:
 # azurerm_app_service.main will no longer be managed by Terraform, but will not be destroyed
 # (destroy = false is set in the configuration)
 . resource "azurerm_app_service" "main" {
        id                                = "/subscriptions/32cfe0af-c5cf-4a55-9d85-897b85a8f07c/resourceGroups/rg-ep112-5w97hr/providers/Microsoft.Web/sites/app-ep112-5w97hr"
        name                              = "app-ep112-5w97hr"
        tags                              = {}
        # (17 unchanged attributes hidden)
        # (5 unchanged blocks hidden)
    }
Plan: 0 to add, 0 to change, 0 to destroy.

Now that the apply is complete we can remove the removed block for the App Service since the removal has already taken place in the last terraform apply.

Remove the App Service 'Plan'

Now let's follow the same procedure for the App Service Plan. First, we need a removed block:

removed {
  from = azurerm_app_service_plan.main
  lifecycle {
    destroy = false
  }
}

Make sure to delete the old App Service Plan's resource block from the code and run terraform apply. Now we are cooking with gas.

Import the App Service Plan

Now that we have eradicated both our App Service Plan and our App Service from Terraform state we can commence the importation process.

When I outputted the Azure Resource ID for my App Service Plan it came back incorrectly.

Error: parsing "/subscriptions/32cfe0af-c5cf-4a55-9d85-897b85a8f07c/resourceGroups/rg-ep112-5w97hr/providers/Microsoft.Web/serverfarms/asp-ep112-5w97hr": parsing segment "staticServerFarms": parsing the AppServicePlan ID: the segment at position 6 didn't match
│ 
│ Expected a AppServicePlan ID that matched:
│ 
│ > /subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/example-resource-group/providers/Microsoft.Web/serverFarms/serverFarmValue
│ 
│ However this value was provided:
│ 
│ > /subscriptions/32cfe0af-c5cf-4a55-9d85-897b85a8f07c/resourceGroups/rg-ep112-5w97hr/providers/Microsoft.Web/serverfarms/asp-ep112-5w97hr
│ 
│ The parsed Resource ID was missing a value for the segment at position 6
│ (which should be the literal value "serverFarms").

It seems that the Resource ID had an all-lower case segment for "serverfarms" and ARM wants "serverFarms" with a capital "F".

Changing this small value and EUREKA! It works!

azurerm_service_plan.main: Importing... [id=/subscriptions/32cfe0af-c5cf-4a55-9d85-897b85a8f07c/resourceGroups/rg-ep112-5w97hr/providers/Microsoft.Web/serverFarms/asp-ep112-5w97hr]
azurerm_service_plan.main: Import complete [id=/subscriptions/32cfe0af-c5cf-4a55-9d85-897b85a8f07c/resourceGroups/rg-ep112-5w97hr/providers/Microsoft.Web/serverFarms/asp-ep112-5w97hr]
Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed.

Now our App Service Plan is back under Terraform management!

Import the App Service App

So, we already know that our App Service Plan has an Operating System type of Windows. Therefore it makes picking between azurerm_windows_web_app and azurerm_linux_web_app that much easier.

During the terraform plan for the import we can see the differences between our old configuration and our new configuration.

None

Using a Placeholder Resource can show us what we need to fix

We can see that we are missing app_settings. Which should be easy enough to put back in place.

Under site_config we have a few properties that are getting reset. Some that are included in our original configuration and some that are not. The ones that are not included in our configuration are likely updated defaults that the new version of the provider is supplying to ARM.

  • always_on: true
  • ftps_state: Disabled
  • ip_restriction_default_action: Allow
  • scm_ip_restriction_default_action: Allow
  • use_32_bit_worker: true

The application_stack is a block under the site_config nested block. I think the only value we are setting is dotnet_version.

azurerm_windows_web_app.main: Importing... [id=/subscriptions/32cfe0af-c5cf-4a55-9d85-897b85a8f07c/resourceGroups/rg-ep112-5w97hr/providers/Microsoft.Web/sites/app-ep112-5w97hr]
azurerm_windows_web_app.main: Import complete [id=/subscriptions/32cfe0af-c5cf-4a55-9d85-897b85a8f07c/resourceGroups/rg-ep112-5w97hr/providers/Microsoft.Web/sites/app-ep112-5w97hr]
azurerm_windows_web_app.main: Modifying... [id=/subscriptions/32cfe0af-c5cf-4a55-9d85-897b85a8f07c/resourceGroups/rg-ep112-5w97hr/providers/Microsoft.Web/sites/app-ep112-5w97hr]
azurerm_windows_web_app.main: Still modifying... [id=/subscriptions/32cfe0af-c5cf-4a55-9d85-...s/Microsoft.Web/sites/app-ep112-5w97hr, 10s elapsed]
azurerm_windows_web_app.main: Still modifying... [id=/subscriptions/32cfe0af-c5cf-4a55-9d85-...s/Microsoft.Web/sites/app-ep112-5w97hr, 20s elapsed]
azurerm_windows_web_app.main: Still modifying... [id=/subscriptions/32cfe0af-c5cf-4a55-9d85-...s/Microsoft.Web/sites/app-ep112-5w97hr, 30s elapsed]
azurerm_windows_web_app.main: Still modifying... [id=/subscriptions/32cfe0af-c5cf-4a55-9d85-...s/Microsoft.Web/sites/app-ep112-5w97hr, 40s elapsed]
azurerm_windows_web_app.main: Modifications complete after 44s [id=/subscriptions/32cfe0af-c5cf-4a55-9d85-897b85a8f07c/resourceGroups/rg-ep112-5w97hr/providers/Microsoft.Web/sites/app-ep112-5w97hr]
Apply complete! Resources: 1 imported, 0 added, 1 changed, 0 destroyed.

That's it! Now both our App Service Plan and App Service are back under Terraform management using version 3.116.0.

Upgrade to 4.x

Now all we have to do is update the version constraint in our required providers block like we did when we upgraded to 3.x.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0.0"
    }
  }
}

Then run terraform apply and we are now rocking azurerm version 4.0 without any deprecated resource warnings! Now the only question we have to ask ourselves is, "will we wait another three years before upgrading our Terraform providers?"

Wanna see this in action? Check out my YouTube video covering this process.

None
Watch on YouTube