How to deploy an Azure resource using Terraform when it is not available in the AzureRM official provider
Just show me the code!
As always, if you don’t care about the post I have uploaded the source code on my Github.
Everyone who has worked long enough with Terraform in Azure has been in the position of wanting to deploy a resource that’s not available on the official Azure Terraform provider.
The same situation also happens when trying to enable a feature on an existing resource, and that feature is missing from the AzureRM Terraform provider.
A solution to those problems might be to switch to Bicep or Azure ARM Templates, but if all my cloud infrastructure is written as code using Terraform, why should I switch to another tool? What can I do to keep using Terraform?
Well, that’s the point of this post, to review which options we have available when we want to create or update a service on Azure using Terraform, but it is not available on the AzureRM Terraform provider.
Before we dive further into this post, let me remind you that if you can avoid using any of the options we’re going to discuss in this post then do so.
Doing what we’re going to discuss here should be a last resort and it makes sense only when the AzureRM provider doesn’t have an implementation for the service we want to create or update.
Available options for creating or updating an Azure resource when it is not available in the AzureRM provider
Nowadays there are 3 options available:
- Using the
null_resource
alongside with thelocal-exec
provisioner to execute a script that uses theAzure CLI
or theAzure Az Powershell Module
. - Using the
azurerm_resource_group_template_deployment
from the AzureRM provider to deploy anARM Template
. - Using the
AzAPI
provider.
In the following sections we’re going to talk about benefits and downsides for all the above options.
Using the null_resource + local-exec provisioner + Azure CLI or Azure Az Powershell Module
Important: Use this approach as a last resort. The other options discussed in this post are a far better alternative.
The null_resource
is a Terraform resource that doesn’t do anything.
If you need to run provisioners that aren’t directly associated with a specific resource, you can associate them with a null_resource
.
The local-exec
provisioner is used to invoke a local executable on the machine running Terraform.
Pairing the null_resource
with the local-exec
provisioner allows us to define a Terraform resource that can run a local executable on our local machine, such as an external script that uses the Azure CLI to create a new resource on Azure.
The next code snippet shows an example of how you could execute a script that creates an Azure Resource Group using the null_resource
and the local-exec
provisioner.
resource "null_resource" "res_group" {
triggers = {
location = "westeurope"
}
provisioner "local-exec" {
command = "./create_resource_group.sh ${self.triggers.location}"
interpreter = ["bash", "-c"]
}
}
The triggers
argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.
The create_resource_group.sh
script will look like this:
#!/bin/bash
LOCATION="$1"
az group create -l $LOCATION -n MyResourceGroup
How to destroy a resource using the local-exec provisioner
By default, the local-exec
provisioner will run only when the resources they are defined within is created. It only runs during resource creation, not during updating or any other lifecycle.
If we want that our resource gets deleted using the local-exec
provisioner we need to use the when
argument. If when = destroy
argument is specified, the provisioner will run when the resource is destroyed.
The next code snippet shows an example of how you could use the null_resource
and the local-exec
provisioner to run a script that creates an Azure Resource Group and another one that deletes it.
resource "null_resource" "res_group" {
triggers = {
location = "westeurope"
}
provisioner "local-exec" {
command = "./create_resource_group.sh ${self.triggers.location}"
interpreter = ["bash", "-c"]
}
provisioner "local-exec" {
when = destroy
command = "./destroy_resource_group.sh"
interpreter = ["bash", "-c"]
}
}
The destroy_resource_group.sh
script will look like this:
#!/bin/bash
az group delete -n MyResourceGroup --yes
There is one big problem with this approach.
When we remove a resource from the Terraform file and execute the apply
command normally the resource gets deleted from the Terraform state file and from Azure, that’s the common behaviour of Terraform, but it doesn’t apply to the local-exec
provisioner.
If a null_resource
block with a local-exec
provisioner gets removed entirely from the Terraform file the resource won’t be destroyed at all. To work around this, a multi-step process needs to be used to safely remove a resource:
- Update the resource configuration to include count = 0.
- Apply the configuration to destroy any existing instances of the resource, including running the destroy provisioner.
- Remove the resource block entirely from configuration, along with its provisioner blocks.
- Apply again, at which point no further action should be taken since the resources were already destroyed.
The next code snippet shows an example of how you could destroy an Azure Resource Group that’s been created using the null_resource
and the local-exec
provisioner.
resource "null_resource" "res_group" {
count = 0
triggers = {
location = "westeurope"
}
provisioner "local-exec" {
command = "./create_resource_group.sh ${self.triggers.location}"
interpreter = ["bash", "-c"]
}
provisioner "local-exec" {
when = destroy
command = "./destroy_resource_group.sh"
interpreter = ["bash", "-c"]
}
}
Benefits
- The ability to execute any command available in the Azure CLI using Terraform.
Downsides
- The information about the resource we have created or modified is not present in the state file. The only data available in the state file is the one present on the
triggers
attribute. - If an error is thrown during the script execution, your state file might end up in an inconsistent state. It depends on how you build the script.
- Destroying a resource requires a multi-step process.
- Everytime you want to provision a resource you have to write 2 scripts: one for provisioning the resource and another one for destroying it.
- You can’t run an in-place update on a resource in a subsequent
apply
command, you can only destroy and recreate the resource.
Using the azurerm_resource_group_template_deployment resource + ARM Template
The azurerm_resource_group_template_deployment
is a resource from the official AzureRM Terraform provider and it allows us to manage a Resource Group Template Deployment using an ARM template.
This approach is the equivalent of deploying an Azure ARM Template, but using Terraform to do it.
The next code snippet shows an example of how you could use the azurerm_resource_group_template_deployment
to manage an Azure Network Watcher
.
resource "azurerm_resource_group" "res_group" {
name = "rg-test"
location = "West US"
}
resource "azurerm_resource_group_template_deployment" "network_watcher" {
name = "network-watcher-deployment"
resource_group_name = azurerm_resource_group.res_group.name
deployment_mode = "Incremental"
template_content = file("template.json")
parameters_content = <<PARAMETERS
{
"name": {
"value": "network-wather-example"
},
"location": {
"value": "${azurerm_resource_group.res_group.location}"
}
}
PARAMETERS
depends_on = [
azurerm_resource_group.res_group
]
}
And the ARM Template looks like this:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"name": {
"type": "String"
},
"location": {
"type": "String"
}
},
"variables": {},
"resources": [
{
"type": "Microsoft.Network/networkWatchers",
"apiVersion": "2022-01-01",
"name": "[parameters('name')]",
"location": "[parameters('location')]",
"properties": {}
}
]
}
Benefits
- The ability to deploy any resource available on the Azure ARM REST Api.
Downsides
- When an ARM template execution fails in Terraform, Terraform doesn’t record the fact that the deployment was physically created in the state file. Which means that the next time you try to deploy the Terraform ARM template it will be error out, from this point forward the only solution is to manually delete the Azure deployment.
- Terraform only understands changes made to the ARM template. If you modify a resource directly in Azure, Terraform is not capable to pick up those changes.
- It is not easy if you want to update an attribute of an existing resource. You’ll have to describe the entire resource on your ARM template even if you only want to update a single property of the existing resource.
Using the AzAPI provider
Important: Right now, using the
AzApi
provider is the best available solution when trying to create or update an Azure resource that is missing in the AzureRM provider.
The AzAPI
provider is a very thin layer on top of the Azure ARM REST APIs (This API is the same one that is used when we deploy an ARM Template).
The AzAPI
provider contains only 3 resources:
azapi_resource
: Used for managing Azure resources.azapi_update_resource
: Used to add or modify properties on an existing resource. If you delete anazapi_update_resource
block from your Terraform file, no operation will be performed and these properties will stay unchanged. If you want to restore the modified properties, you must re-apply the restored properties before deleting.azapi_resource_action
: Used to perform any resource action. It’s recommended to use this resource to perform actions which change a resource state.
The next code snippet shows an example of how you could use the AzApi
provider to manage an Azure DNS Resolver
.
resource "azurerm_resource_group" "res_group" {
name = "rg-test"
location = "West US"
}
resource "azurerm_virtual_network" "vnet" {
name = "vnet-test"
location = azurerm_resource_group.res_group.location
resource_group_name = azurerm_resource_group.res_group.name
address_space = ["10.18.0.0/16"]
}
resource "azapi_resource" "dns_resolver" {
type = "Microsoft.Network/dnsResolvers@2020-04-01-preview"
name = "resolver-test"
parent_id = azurerm_resource_group.res_group.id
location = azurerm_resource_group.res_group.location
body = jsonencode({
properties = {
virtualNetwork = {
id = azurerm_virtual_network.vnet.id
}
}
})
response_export_values = ["*"]
}
When creating or update a resource using the AzApi
resource you’ll need to specify the following attributes:
-
type
: The value for this field is the resource-type and the api-version, following the convention:<resource-type>@<api-version>
as an exampleMicrosoft.Network/dnsResolvers@2020-04-01-preview
. -
parent_id
Is the Id of the resource or item is deployed within, such as a resource group id. -
body
: Is the JSON object that contains the request body used to either create or update the Azure resource in question.
Documentation
This is the “go to” website when using the AzApi
provider:
You’ll find an inventory of all the Azure resources available and how to create them using the AzApi
provider.
Benefits
- The ability to deploy any resource available on the Azure ARM REST Api.
- Full Terraform state file fidelity.
- It is really easy to update an attribute on an existing resource.
- There is no need to use any external file, such as an script or an ARM Template.
- It is the most “Terraform syntax friendly” from the 3 options we have discussed in this post.
Downsides
- The fact that the
body
attribute is ajsonencode
object might sometimes show you a false change when running theterraform plan
command.
Examples
During this post we have seen some really simple examples of how to create or update an Azure resource using the local-exec
provisioner, the azurerm_resource_group_template_deployment
resource or the AzApi
provider.
Before ending this post, let me show you a couple more complex ones.
Example 1: Create an Azure App Configuration
I’m aware that you can create an Azure App Configuration
using the azurerm_app_configuration
resource available on the AzureRM provider, but this is just an example of how you can provision an Azure resource using Terraform without the AzureRM provider.
Using the null_resource, local-exec provisioner and the AZ CLI
- This example uses:
- An
azurerm_resource_group
to create a resource group. - A
null_resource
with a couple oflocal-exec
provisioners to create theAzure App Configuration
.- The first
local-exec
provisioner is triggered when the resource needs to be created and it invokes thecreate_app_config.sh
script. - The second
local-exec
provisioner contains awhen = destroy
attribute, which mean that it will be triggered when the resource needs to be destroyed. This second provisioner will invoke thedestroy_app_config.sh
script.
- The first
- The
Azure App Configuration
creation/deletion is done inside the Shell scripts.- It uses the
AZ CLI
to create or delete theApp Configuration
.
- It uses the
- An
Here’s how the Terraform file looks like:
locals {
resource_group_name = "rg-provisioning-demo"
app_conf_name = "appconf-demo-dev"
app_conf_sku = "Free"
app_conf_enable_public_network = false
app_conf_disable_local_auth = false
app_conf_location = "westeurope"
}
## Create Resource Group
resource "azurerm_resource_group" "rg_demo" {
name = local.resource_group_name
location = "West Europe"
}
## Create App Configuration using an external script
resource "null_resource" "app_conf" {
triggers = {
app_conf_name = local.app_conf_name
res_group_name = local.resource_group_name
sku = local.app_conf_sku
enable_public_network = local.app_conf_enable_public_network
disable_local_auth = local.app_conf_disable_local_auth
location = local.app_conf_location
}
provisioner "local-exec" {
command = "./create_app_config.sh ${self.triggers.app_conf_name} ${self.triggers.res_group_name} ${self.triggers.sku} ${self.triggers.enable_public_network} ${self.triggers.disable_local_auth} ${self.triggers.location}"
interpreter = ["bash", "-c"]
}
provisioner "local-exec" {
when = destroy
command = "./destroy_app_config.sh ${self.triggers.app_conf_name} ${self.triggers.res_group_name} ${self.triggers.sku} ${self.triggers.enable_public_network} ${self.triggers.disable_local_auth}"
interpreter = ["bash", "-c"]
}
depends_on = [
azurerm_resource_group.rg_demo
]
}
The create_app_config.sh
script executes the following steps:
- It checks if an App Configuration with the given name already exists.
- If it doesn’t exists, it creates a new
App Configuration
using theaz appconfig create
command.
#!/bin/bash
APP_CONFIG_NAME="$1"
RES_GROUP_NAME="$2"
SKU="$3"
ENABLE_PUBLIC_NETWORK="$4"
DISABLE_LOCAL_AUTH="$5"
LOCATION="$6"
app_config_instance=$(az appconfig list | jq --arg name "$APP_CONFIG_NAME" -e '.[]|select(.name==$name).name')
if [ -z "$app_config_instance" ]; then
echo "App Config does not exists. Creating a new one."
az appconfig create --name $APP_CONFIG_NAME --resource-group $RES_GROUP_NAME --sku $SKU --enable-public-network $ENABLE_PUBLIC_NETWORK --disable-local-auth $DISABLE_LOCAL_AUTH --location $LOCATION
fi
The destroy_app_config.sh
script executes the following steps:
- It checks if an App Configuration with the given name already exists.
- If it exists, it deletes the
App Configuration
using theaz appconfig delete
command. - Sleeps during 30 seconds.
The local-exec
provisioner can’t run an in-place update on a resource in a subsequent terraform apply
command, the only option available is to delete it and afterwards recreate it with the new updated attributes, which means that the App Configuration
creation happens instantly after the deletion, so an error might appear saying that the App Configuration
still exists, that’s the reason why we’re executing a sleep
command after deleting the resource, to let Azure enough time to catch up.
#!/bin/bash
APP_CONFIG_NAME="$1"
RES_GROUP_NAME="$2"
SKU="$3"
ENABLE_PUBLIC_NETWORK="$4"
DISABLE_LOCAL_AUTH="$5"
app_config_instance=$(az appconfig list | jq --arg name "$APP_CONFIG_NAME" -e '.[]|select(.name==$name).name')
if [ -z "$app_config_instance" ]; then
echo "App Config does not exist. No need to delete anything."
else
echo "App Config found. Trying to destroy the resource."
az appconfig delete --name $APP_CONFIG_NAME --resource-group $RES_GROUP_NAME --yes
sleep 30s
fi
Using the azurerm_resource_group_template_deployment resource
- This example uses:
- An
azurerm_resource_group
to create a resource group. - An
azurerm_resource_group_template_deployment
to create theApp Configuration
- This resource uses an ARM Template that can be found on the
template.json
file. - Parameters can be passed from the Terraform file to the ARM template using the
parameters_content
attribute.
- This resource uses an ARM Template that can be found on the
- An
Here’s how the Terraform file looks like:
locals {
resource_group_name = "rg-provisioning-demo"
app_conf_name = "appconf-demo-dev"
app_conf_sku = "free"
app_conf_public_network_access = "Enabled"
app_conf_disable_local_auth = false
app_conf_location = "westeurope"
}
## Create Resource Group
resource "azurerm_resource_group" "rg_demo" {
name = local.resource_group_name
location = "West Europe"
}
## Create App Configuration using the resource group arm template resource
resource "azurerm_resource_group_template_deployment" "appconf" {
name = "app-conf-deploy"
resource_group_name = local.resource_group_name
deployment_mode = "Incremental"
template_content = file("template.json")
parameters_content = <<PARAMETERS
{
"name": {
"value": "${local.app_conf_name}"
},
"location": {
"value": "${local.app_conf_location}"
},
"sku": {
"value": "${local.app_conf_sku}"
},
"public_network_access": {
"value": "${local.app_conf_public_network_access}"
},
"disable_local_auth": {
"value": "${local.app_conf_disable_local_auth}"
}
}
PARAMETERS
depends_on = [
azurerm_resource_group.rg_demo
]
}
And here’s how the ARM template file looks like:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"name": {
"type": "String",
"metadata": {
"description": "Specifies the name of the App Configuration."
}
},
"location": {
"type": "String",
"metadata": {
"description": "Specifies the location of the App Configuration."
}
},
"sku": {
"type": "String",
"metadata": {
"description": "The SKU name of the App Configuration"
}
},
"public_network_access": {
"type": "String",
"metadata": {
"description": "The Public Network Access setting of the App Configuration"
}
},
"disable_local_auth": {
"type": "String",
"metadata": {
"description": "Whether local authentication methods is enabled."
}
}
},
"variables": {},
"resources": [
{
"type": "Microsoft.AppConfiguration/configurationStores",
"apiVersion": "2022-05-01",
"name": "[parameters('name')]",
"location": "[parameters('location')]",
"sku": {
"name": "[parameters('sku')]"
},
"properties": {
"encryption": {},
"publicNetworkAccess": "[parameters('public_network_access')]",
"disableLocalAuth": "[parameters('disable_local_auth')]",
"softDeleteRetentionInDays": 0,
"enablePurgeProtection": false
}
}
]
}
Using AzApi provider
- This example uses:
- An
azurerm_resource_group
to create a resource group. - An
azapi_resource
to create theApp Configuration
.
- An
Here’s how the Terraform file looks like:
locals {
resource_group_name = "rg-provisioning-demo"
app_conf_name = "appconf-demo-dev"
app_conf_sku = "free"
app_conf_public_network_access = "Enabled"
app_conf_disable_local_auth = false
}
## Create Resource Group
resource "azurerm_resource_group" "rg_demo" {
name = local.resource_group_name
location = "West Europe"
}
## Create App Configuration using the AzApi provider
resource "azapi_resource" "appconf" {
type = "Microsoft.AppConfiguration/configurationStores@2022-05-01"
name = local.app_conf_name
parent_id = azurerm_resource_group.rg_demo.id
location = azurerm_resource_group.rg_demo.location
body = jsonencode({
sku = {
name = local.app_conf_sku
}
properties = {
publicNetworkAccess = local.app_conf_public_network_access
disableLocalAuth = local.app_conf_disable_local_auth
}
})
response_export_values = ["*"]
}
Example 2: Enable SFTP support on an existing Azure Storage Account
This is an example that shows how to update an existing resource without using the AzureRM Terraform provider.
To be more precise we’re going to enable the SFTP support for Azure Blob Storage on an existing Azure Storage Account.
Right now (11/09/2022) it is impossible to enable the SFTP support for Azure Blob Storage using the AzureRM Terraform provider, so this is going to be a more realistic example than the previous one.
Using the null_resource, local-exec provisioner and the AZ CLI
- This example uses:
- An
azurerm_resource_group
to create a resource group. - An
azurerm_storage_account
resource to create a storage account. - An
azurerm_storage_container
resource to create a container within the storage account. - A
null_resource
with a couple oflocal-exec
provisioners to enable the SFTP support and create a local user with permissions to access the SFTP.- The first
local-exec
provisioner executes theenable_sftp_create_localuser.sh
script that enables the SFTP support and creates an SFTP local user. - The second
local-exec
provisioner contains thewhen = destroy
attribute. This provisioner is triggered when the resource needs to be destroyed and it invokes thedisable_sftp_and_localuser.sh
script that deletes the SFTP local user.
- The first
- Enabling or disabling the SFTP support is done via Shell script.
- The Shell script uses the
AZ CLI
to enable or disable the SFTP support.
- The Shell script uses the
- An
Here’s how the Terraform file looks like:
locals {
resource_group_name = "rg-provisioning-demo"
storage_account_name = "stsftprovdev"
}
## Create Resource Group
resource "azurerm_resource_group" "rg_demo" {
name = local.resource_group_name
location = "West Europe"
}
## Create Storage Account
resource "azurerm_storage_account" "sftp_storage_acct" {
name = local.storage_account_name
location = azurerm_resource_group.rg_demo.location
resource_group_name = azurerm_resource_group.rg_demo.name
account_tier = "Standard"
account_replication_type = "LRS"
min_tls_version = "TLS1_2"
is_hns_enabled = true
}
# Create container
resource "azurerm_storage_container" "azurerm_storage_container" {
name = "container"
storage_account_name = azurerm_storage_account.sftp_storage_acct.name
}
## Enable SFTP and create SFTP local users using an external script
resource "null_resource" "sftp_enable" {
triggers = {
storage_account_name = local.storage_account_name
res_group_name = local.resource_group_name
sftp_user = "ftpuser"
}
provisioner "local-exec" {
command = "./enable_sftp_create_localuser.sh ${self.triggers.storage_account_name} ${self.triggers.res_group_name} ${self.triggers.sftp_user}"
interpreter = ["bash", "-c"]
}
provisioner "local-exec" {
when = destroy
command = "./disable_sftp_and_localuser.sh ${self.triggers.storage_account_name} ${self.triggers.res_group_name} ${self.triggers.sftp_user}"
interpreter = ["bash", "-c"]
}
depends_on = [
azurerm_storage_container.azurerm_storage_container
]
}
The enable_sftp_create_localuser.sh
script executes the following steps:
- Checks if the
AllowSFTP
feature is enabled in your Azure subscription, if it is not enabled it throws and error. - Checks if the
az storage-preview
extension is installed on your machine. If it is not installed, it installs it. - Enables SFTP support on the storage account.
- Creates a SFTP local user.
- Retrieves the user password.
#!/bin/bash
STORAGE_ACCT_NAME="$1"
RES_GROUP_NAME="$2"
SFTP_USER="$3"
state=$(az feature show --namespace Microsoft.Storage --name AllowSFTP | jq '.properties.state')
if [ $state != '"Registered"' ]; then
echo "Feature not registered. Registration is an asynchronous operation, it must be done manually."
exit 1
fi
extension=$(az extension list | jq -e '.[]|select(.name=="storage-preview").name')
if [ -z "$extension" ]; then
echo "Storage-preview extension missing. Installing it."
az extension add -n storage-preview
fi
az storage account update -g $RES_GROUP_NAME -n $STORAGE_ACCT_NAME --enable-sftp true
az storage account local-user create --account-name $STORAGE_ACCT_NAME -g $RES_GROUP_NAME -n $SFTP_USER --home-directory "container" --has-ssh-password true --has-ssh-key true --permission-scope permissions=rw service=blob resource-name=container
az storage account local-user regenerate-password --account-name $STORAGE_ACCT_NAME -g $RES_GROUP_NAME -n $SFTP_USER
The disable_sftp_and_localuser.sh
script executes the following steps:
- Deletes the SFTP local user.
#!/bin/bash
STORAGE_ACCT_NAME="$1"
RES_GROUP_NAME="$2"
SFTP_USER="$3"
az storage account local-user delete --account-name $STORAGE_ACCT_NAME -g $RES_GROUP_NAME -n $SFTP_USER
Using the azurerm_resource_group_template_deployment resource
- This example uses:
- An
azurerm_resource_group
to create a resource group. - An
azurerm_storage_account
resource to create a storage account. - An
azurerm_storage_container
resource to create a container within the storage account. - An
azurerm_resource_group_template_deployment
to enable the SFTP support and to create a SFTP local user.- The
azurerm_resource_group_template_deployment
resource uses an ARM Template that can be found on thetemplate.json
file. - To enable SFTP support we only need to set the Storage Account
isSftpEnabled
attribute totrue
, but the entire object must be described nonetheless. - Any change we make on the Terraform
azurerm_storage_account
resource must also be changed on the ARM template, or it will get overriden. - Parameters can be passed from the Terraform file to the ARM template using the
parameters_content
attribute.
- The
- An
Here’s how the Terraform file looks like:
locals {
resource_group_name = "rg-provisioning-demo"
storage_account_name = "stsftprovdev"
storage_account_tier = "Standard"
storage_account_replication = "LRS"
storage_account_min_tls = "TLS1_2"
storage_account_hns_enabled = true
sftp_user = "ftpuser2"
}
## Create Resource Group
resource "azurerm_resource_group" "rg_demo" {
name = local.resource_group_name
location = "West Europe"
}
## Create Storage Account
resource "azurerm_storage_account" "sftp_storage_acct" {
name = local.storage_account_name
location = azurerm_resource_group.rg_demo.location
resource_group_name = azurerm_resource_group.rg_demo.name
account_tier = local.storage_account_tier
account_replication_type = local.storage_account_replication
min_tls_version = local.storage_account_min_tls
is_hns_enabled = local.storage_account_hns_enabled
}
# Create container
resource "azurerm_storage_container" "azurerm_storage_container" {
name = "container"
storage_account_name = azurerm_storage_account.sftp_storage_acct.name
}
## Enable SFTP and add local users using the resource group arm template resource
resource "azurerm_resource_group_template_deployment" "sftp" {
name = "sftp-deploy"
resource_group_name = local.resource_group_name
deployment_mode = "Incremental"
template_content = file("template.json")
parameters_content = <<PARAMETERS
{
"storage_account_name": {
"value": "${local.storage_account_name}"
},
"storage_account_tier": {
"value": "${local.storage_account_tier}"
},
"storage_account_replication": {
"value": "${local.storage_account_replication}"
},
"storage_account_min_tls": {
"value": "${local.storage_account_min_tls}"
},
"storage_account_hns_enabled": {
"value": ${local.storage_account_hns_enabled}
},
"sftp_user": {
"value": "${local.sftp_user}"
}
}
PARAMETERS
depends_on = [
azurerm_resource_group.rg_demo,
azurerm_storage_account.sftp_storage_acct,
azurerm_storage_container.azurerm_storage_container
]
}
And here’s how the ARM template file looks like:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"storage_account_name": {
"type": "String",
"metadata": {
"description": "Specifies the name of the Storage Account."
}
},
"storage_account_tier": {
"type": "String",
"metadata": {
"description": "Defines the Tier to use for this storage account."
}
},
"storage_account_replication": {
"type": "String",
"metadata": {
"description": "Defines the type of replication to use for this storage account."
}
},
"storage_account_min_tls": {
"type": "String",
"metadata": {
"description": "The minimum supported TLS version for the storage account."
}
},
"storage_account_hns_enabled": {
"type": "Bool",
"metadata": {
"description": "Is Hierarchical Namespace enabled?."
}
},
"sftp_user": {
"type": "String",
"metadata": {
"description": "The SFTP local username."
}
}
},
"variables": {},
"resources": [
{
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2022-05-01",
"name": "[parameters('storage_account_name')]",
"location": "westeurope",
"sku": {
"name": "[concat(parameters('storage_account_tier'), '_', parameters('storage_account_replication'))]",
"tier": "[parameters('storage_account_tier')]"
},
"kind": "StorageV2",
"identity": {
"type": "None"
},
"properties": {
"defaultToOAuthAuthentication": false,
"publicNetworkAccess": "Enabled",
"allowCrossTenantReplication": true,
"isNfsV3Enabled": false,
"isLocalUserEnabled": true,
"isSftpEnabled": true,
"minimumTlsVersion": "[parameters('storage_account_min_tls')]",
"allowBlobPublicAccess": true,
"allowSharedKeyAccess": true,
"isHnsEnabled": "[parameters('storage_account_hns_enabled')]",
"networkAcls": {
"bypass": "AzureServices",
"virtualNetworkRules": [],
"ipRules": [],
"defaultAction": "Allow"
},
"supportsHttpsTrafficOnly": true,
"encryption": {
"services": {
"file": {
"keyType": "Account",
"enabled": true
},
"blob": {
"keyType": "Account",
"enabled": true
}
},
"keySource": "Microsoft.Storage"
},
"accessTier": "Hot"
}
},
{
"type": "Microsoft.Storage/storageAccounts/blobServices",
"apiVersion": "2022-05-01",
"name": "[concat(parameters('storage_account_name'), '/default')]",
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts', parameters('storage_account_name'))]"
],
"sku": {
"name": "[concat(parameters('storage_account_tier'), '_', parameters('storage_account_replication'))]",
"tier": "[parameters('storage_account_tier')]"
},
"properties": {
"cors": {
"corsRules": []
},
"deleteRetentionPolicy": {
"allowPermanentDelete": false,
"enabled": false
}
}
},
{
"type": "Microsoft.Storage/storageAccounts/localusers",
"apiVersion": "2022-05-01",
"name": "[concat(parameters('storage_account_name'), '/', parameters('sftp_user'))]",
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts', parameters('storage_account_name'))]"
],
"properties": {
"hasSshPassword": true,
"permissionScopes": [
{
"permissions": "rw",
"service": "blob",
"resourceName": "container"
}
],
"homeDirectory": "container",
"hasSharedKey": false,
"hasSshKey": false
}
},
{
"type": "Microsoft.Storage/storageAccounts/blobServices/containers",
"apiVersion": "2022-05-01",
"name": "[concat(parameters('storage_account_name'), '/default/container')]",
"dependsOn": [
"[resourceId('Microsoft.Storage/storageAccounts/blobServices', parameters('storage_account_name'), 'default')]",
"[resourceId('Microsoft.Storage/storageAccounts', parameters('storage_account_name'))]"
],
"properties": {
"defaultEncryptionScope": "$account-encryption-key",
"denyEncryptionScopeOverride": false,
"publicAccess": "None"
}
}
]
}
Using AzApi provider
- This example uses:
- An
azurerm_resource_group
to create a resource group. - An
azurerm_storage_account
resource to create a storage account. - An
azurerm_storage_container
resource to create a container within the storage account. - An
azapi_update_resource
to enable the SFTP support. - An
azapi_resource
to create a SFTP local user. - An
azapi_resource_action
to retrieve the user password.
- An
Here’s how the Terraform file looks like:
locals {
resource_group_name = "rg-provisioning-demo"
}
## Create Resource Group
resource "azurerm_resource_group" "rg_demo" {
name = local.resource_group_name
location = "West Europe"
}
## Create Storage Account
resource "azurerm_storage_account" "sftp_storage_acct" {
name = "stsftprovdev"
location = azurerm_resource_group.rg_demo.location
resource_group_name = azurerm_resource_group.rg_demo.name
account_tier = "Standard"
account_replication_type = "LRS"
min_tls_version = "TLS1_2"
is_hns_enabled = true
}
# Create container
resource "azurerm_storage_container" "sftp_storage_acct_container" {
name = "container"
storage_account_name = azurerm_storage_account.sftp_storage_acct.name
}
# Enable SFTP
resource "azapi_update_resource" "sftp_azpi_sftp" {
type = "Microsoft.Storage/storageAccounts@2021-09-01"
resource_id = azurerm_storage_account.sftp_storage_acct.id
body = jsonencode({
properties = {
isSftpEnabled = true
}
})
depends_on = [
azurerm_storage_account.sftp_storage_acct,
azurerm_storage_container.sftp_storage_acct_container
]
response_export_values = ["*"]
}
# Create local user
resource "azapi_resource" "sftp_local_user" {
type = "Microsoft.Storage/storageAccounts/localUsers@2021-09-01"
parent_id = azurerm_storage_account.sftp_storage_acct.id
name = "ftpuser"
body = jsonencode({
properties = {
hasSshPassword = true,
homeDirectory = "container"
hasSharedKey = true,
hasSshKey = false,
permissionScopes = [{
permissions = "rl",
service = "blob",
resourceName = "container"
}]
}
})
response_export_values = ["*"]
depends_on = [
azurerm_storage_account.sftp_storage_acct,
azurerm_storage_container.sftp_storage_acct_container,
azapi_update_resource.sftp_azpi_sftp
]
}
# Retrieve password
resource "azapi_resource_action" "generate_sftp_user_password" {
type = "Microsoft.Storage/storageAccounts/localUsers@2022-05-01"
resource_id = azapi_resource.sftp_local_user.id
action = "regeneratePassword"
body = jsonencode({
username = azapi_resource.sftp_local_user.name
})
response_export_values = ["sshPassword"]
depends_on = [
azurerm_storage_account.sftp_storage_acct,
azurerm_storage_container.sftp_storage_acct_container,
azapi_update_resource.sftp_azpi_sftp,
azapi_resource.sftp_local_user
]
}
Useful links
- https://learn.microsoft.com/es-es/azure/templates/?view=azurermps-6.0.0
- https://registry.terraform.io/providers/Azure/azapi/1.0.0
- https://registry.terraform.io/providers/hashicorp/azurerm/3.30.0
- https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group_template_deployment