Terraforming with a Twist: Go with the (azapi) Flow
Tags: terraform, azure, tutorial, devops
Published: 2024-02-18 19:25:30

Sometimes we find ourselves with new default resource configurations which are incompatible with previously deployed infrastructure. These configurations may not even exist inside of a given Terraform provider.

In this post we go over one such example I'd discovered, how I worked around the issue, and some takeaways I learned through the experience.

## 🚨 Hello Error
![HTTP 409 meme](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xi24nbrkx5h2s4jfsh0u.png)
_credit, original link: https://http.cat/status/409_

Recently, while repurposing some code to enable vulnerability assessments on Azure SQL databases, I came across the following error:

> Status=409 Code="VulnerabilityAssessmentStoragelessIsEnabled" Message="Vulnerability Assessment is enabled on this server or one of its underlying databases with an incompatible version. Additional troubleshooting can be found at https://aka.ms/SQLVAStoragelessDocumentation

I didn't understand at the time that this actually had little to do with the resource I was trying to provision (`azurerm_mssql_server_vulnerability_assessment`) and more to do with how Azure now auto-enables a new type of vulnerability assessments on newly provisioned MSSQL servers.
 
## 🔍 Sleuthing for a Fix
Upon initial search for the usual suspects (Stack Overflow, GitHub, cloud/provider documentation), I didn't come across others running into this particular error. Luckily, all was not lost. With the link from the error itself([https://aka.ms/SQLVAStoragelessDocumentation](https://aka.ms/SQLVAStoragelessDocumentation)) I was able to educate myself on exactly what was happening.

This configuration was new to me, so I first needed to learn what the difference between the "express" configuration in comparison to the "classic" configuration. This was provided on the error's link [here](https://learn.microsoft.com/en-us/azure/defender-for-cloud/sql-azure-vulnerability-assessment-overview#whats-the-difference-between-the-express-and-classic-configuration).

After some digging from the resources provided, I learned a crucial piece of info from the [common questions](https://learn.microsoft.com/en-us/azure/defender-for-cloud/faq-defender-for-databases#can-i-choose-which-experience-is-the-default-) doc:
> Q: Can I choose which experience is the default?
> A: No. Express configuration is the default for every new supported Azure SQL database.

Learning that this was auto-configured by default, the next step is to determine how to work through configuring this in Terraform, which led me to [this](https://github.com/hashicorp/terraform-provider-azurerm/issues/19971) GitHub issue (which essentially is simply a request for getting the express configuration of SQL 
server vulnerability assessments supported in the `azurerm` provider.)

So this presented a dead end... or did it? ...

## 🚦 Decision Point - Classic, or Express?

I now needed to make a decision as to which type of Azure SQL Vulnerability Assessment to use:
* Either the new "express" configuration (which was created outside of Terraform automatically by Azure and does not currently [as of 2/16/2024] have support directly in the `azurerm` provider)
* Or somehow disable the "express" configuration (which was causing the original error) and then continue to provision the "classic" configuration 

I decided that using the classic configuration (at least for now) would provide the easiest configuration for our customer. The tradeoffs we made were negligible in this case, since:
1. Classic version has an internal scheduler for sending emails automatically, meaning we would not need to [stand up a separate Logic Apps](https://learn.microsoft.com/en-us/azure/defender-for-cloud/faq-defender-for-databases#is-there-a-way-with-express-configuration-to-get-the-weekly-email-report-that-is-provided-in-the-classic-configuration-) to get this functionality.
2. Requires a storage account dependency, and this is a simple resource to provision.
3. Other minor changes, like what the policy could apply to (Database granularity), or what the scans export format could be (classic can export to Excel).

You can read more about the differences between express and classic configuration [here](https://learn.microsoft.com/en-us/azure/defender-for-cloud/sql-azure-vulnerability-assessment-overview#whats-the-difference-between-the-express-and-classic-configuration).


## 💡 Idea & Testing - Introducing `azapi`

With the decision made to use classic configuration, the main question was:
> How do we disable the express configuration if it's not available in the `azurerm` provider?

Luckily, the [GitHub issue](https://github.com/hashicorp/terraform-provider-azurerm/issues/19971) from sleuthing provided a very useful bit of code that shows it's possible to configure "express" mode simply from an ARM/Bicep template!

But wait, that's not Terraform!

Well, what if I told you that actually, there's a much simpler way to write ARM or Bicep code directly from your Terraform?

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v282qf3xc2d76jlxnou5.png)

The `azapi` provider is pretty cool and easy to get started with. Here are some of my favorite ways it works:

1. Can re-use the same provider credentials as used in azurerm (but doesn't have to). Theoretically, if you have Terraform setup with azurerm, it should just work out of the box with azapi!
2. Acts as almost a shim to ARM or Bicep templates - you only need to specify the body which, thanks to [`jsonencode()`](https://developer.hashicorp.com/terraform/language/functions/jsonencode) can also be done in HCL 🤓. Of course, you can also just use something like a [heredoc](https://developer.hashicorp.com/terraform/language/expressions/strings#heredoc-strings) to write actual JSON from the ARM template here too.

For more info on the `azapi` provider, check out the official docs [here](https://learn.microsoft.com/en-us/azure/developer/terraform/overview-azapi-provider)

## ✅ Final Implementation
You can find a full Terraform deployment [here](https://github.com/codycodes/blog-resources/blob/main/terraform/mssql-vulnerability-assessments-classic-azapi/main.tf), with an abridged (sql-only) subset below:
```hcl
# ---------
# SQL
# ---------
resource "azurerm_mssql_server" "example" {
  name                         = "mysqlserver${random_id.sql_server.hex}"
  resource_group_name          = azurerm_resource_group.example.name
  location                     = azurerm_resource_group.example.location
  version                      = "12.0"
  administrator_login          = "4dm1n157r470r"
  administrator_login_password = "4-v3ry-53cr37-p455w0rd"
}

resource "azurerm_mssql_server_security_alert_policy" "example" {
  resource_group_name = azurerm_resource_group.example.name
  server_name         = azurerm_mssql_server.example.name
  state               = "Enabled"
}

# Use a descriptive name for the terraform resource so it is easy to infer from the plan
# and continued management of said resource
resource "azapi_update_resource" "disable_sql_vulnerability_assessments_express_configuration" {
  type      = "Microsoft.Sql/servers/sqlVulnerabilityAssessments@2022-05-01-preview"
  name      = "default"
  parent_id = azurerm_mssql_server.example.id
  body = jsonencode({
    properties = {
      state = "Disabled"
    }
  })
}

resource "azurerm_mssql_server_vulnerability_assessment" "example" {
  server_security_alert_policy_id = azurerm_mssql_server_security_alert_policy.example.id
  storage_container_path          = "${azurerm_storage_account.example.primary_blob_endpoint}${azurerm_storage_container.example.name}/"
  storage_account_access_key      = azurerm_storage_account.example.primary_access_key

  recurring_scans {
    enabled                   = true
    email_subscription_admins = true
    emails = [
      "email@example1.com",
      "email@example2.com"
    ]
  }
  # Ensure that express configuration is disabled before we provision classic
  depends_on = [azapi_update_resource.disable_sql_vulnerability_assessments_express_configuration]
}
```

A nice graph of all the resources to be provisioned can be found below
(this was generated using `terraform graph | dot -Tpng -o graph.png` after installing `graphviz` on macOS)

![Graph of Resources to Provision](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/irbjeppajd3hhbczgnn1.png)

## 🔑 Main Takeaways
- 🔄 Change is inevitable
  - 🔧🚫 Provisioning code that used to work may become incompatible due to updated external API changes.
- 🤖 `azapi` makes sense to use when external API changes remain to be implemented officially
  - 🔍🔄 `azurerm`'s status of the resource/config in question's implementation due to the changes are important to consider first.
- 🛑 The best way to use `azapi` is not to use it at all
  - 🔄➡️ When `azurerm` is updated with the changes that were implemented in `azapi`, it's best to migrate resources to `azurerm` as it more tightly integrates with Terraform to enable features like intellisense (autocomplete and syntax validation).

## 🎁 Wrapping Up

I'm hopeful this was a helpful blog post on your cloud journey ☁

How have you used `azapi` in your past Terraform deployments? Any comments or clarifications I could use to improve this post? Please feel free to reach out in the comments section below.

All code in the post can be found here https://github.com/codycodes/blog-resources

Thanks, and happy automating ✨
-codycodes