Azure - Create Function App hostkey with Terraform azapi/bicep/powershell - azure

I'm working on automating the rotation of my azure function app's host key, which is used to maintain a more secure connection between my API Management and my function apps. The issue is that I can not figure out how to accomplish this based on the lack of clear documentation. I found a document for how to create a key for a specific function within the function app, but not for the host level. I've tried using the web ui resource manager to figure out what the proper values are, but host seems to have no values available by GET request to help me see what the formatting needs to be. In fact, I can't find any reference to my function app's host keys anywhere in the resource manager UI. (Of course I can in the portal).
I don't care if it's powershell, bicep, ARM, terraform azapi, whatever, I'd just like to find a way to accomplish the creation of a new hostkey so that I can control it's rotation with terraform. Does anyone know how to accomplish this?
Right now my attempt looks like
resource "azapi_resource" "function_host_key" {
type = "Microsoft.Web/sites/host/functionkeys#2018-11-01"
name = "${azurerm_windows_function_app.api_function.name}-host-key"
parent_id = "${azurerm_windows_function_app.api_function.id}/host"
body = jsonencode({
properties = {
name = "test-key-terraform"
value = "asdfasdfasdfasdfasdfasdfasdf"
}
})
}
I also tried
resource "azapi_resource" "function_host_key" {
type = "Microsoft.Web/sites#2018-11-01"
name = "${azurerm_windows_function_app.api_function.name}-host-key"
parent_id = "${azurerm_windows_function_app.api_function.id}/functionsAppKeys"
location = var.region
}
since it said the body was invalid, but this also throws an error due to there being no body. I'm wondering if this just isn't possible.
I also just tried
resource "azapi_resource" "function_host_key" {
type = "Microsoft.Web/host/functionkeys#2018-11-01"
name = "${azurerm_windows_function_app.api_function.name}-host-key"
parent_id = "${azurerm_windows_function_app.api_function.id}/host"
location = var.region
}
and the result said that it was expecting
parent_id of `parent_id is invalid`: expect ID of `Microsoft.Web/host`
so I'm not sure what that parent_id should be.
I found an example through a bash/powershell script using the azure rest API, but I get a 403 error when I attempt to do it, I can only assume because my function app is secured, but I'm not sure a good way to determine that.
There must be a way to create a key programmatically...
UPDATE
I believe that this has been purposely made impossible now to do with terraform and I need to, as grose and backwards as it may be, use a CLI command in my pipeline. I understand you can do this, but it is (ofc my opinion) that if I am using terraform, I have terraform manage something, not have random CLI commands outside of terraform doing things that TF should be able to manage.
I created a key using az functionapp keys set and that worked, and the output explicitly stated that the type of resource which was created was Microsoft.Web/sites/host/functionKeys, so I went to the Azure Resource Explorer to see what versions were available for this type, since it clearly exists.. and found that nope, azure does not have it listed.
What confuses me is that I see this being done w/ ARM templates and I believe that my code matches theirs, just I'm using AZAPI.. and I get a not found error. Giving up for now

Related

Get inbound ip adress from azurerm_windows_web_app in Terraform

I want to create azurerm_mssql_firewall_rule resource witch allows IP of "Azure windows web app".
When I write it manually it works fine. f.e.:
resource "azurerm_mssql_firewall_rule" "api_cloud" {
name = var.cloud_firewal_rule_name
server_id = azurerm_mssql_server.api.id
start_ip_address = "00.000.000.00"
end_ip_address = "00.000.000.00"
}
I want to get IP address like this start_ip_address/end_ip_address = azurerm_windows_web_app.api.inbound_ip_address.
But there isn't inbound option in azurerm_windows_web_app, I can only access outbound addresses azurerm_windows_web_app.api.outbound_ip_addresses.
Is there is anyway do something like this?
IN SHORT:
How to get this IP address with terraform?
Yes, there is but it's a bit complicated due to the way Terraform works. I used a Linux App Service in my examples but it should work identically for both Windows and Linux versions. Let's go:
So, things are a bit more complicated due to the fact that App Services have quite a big range of possible outbound IP addresses as they are running on a shared infrastructure. Therefore it returns a list with an unknown length. That makes things annoying for Terraform. As an example, this is how you usually iterate through multiple items in Terraform using for_each:
resource "azurerm_mssql_firewall_rule" "example" {
for_each = toset(azurerm_linux_web_app.api_app.outbound_ip_address_list)
name = "FirewallRule"
server_id = azurerm_mssql_server.example.id
start_ip_address = each.key
end_ip_address = each.key
}
In this snippet, you take the list of outbound IP addresses from the App Service, cast them to a set, and then iterate through it. However, this only works if the App Service already exists - if you are starting from an empty slate, you will face the following error:
azurerm_linux_web_app.api_app.outbound_ip_address_list is a list of
string, known only after apply
The "for_each" map includes keys
derived from resource attributes that cannot be determined until
apply, and so Terraform cannot determine the full set of keys that
will identify the instances of this resource.
When working with
unknown values in for_each, it's better to define the map keys
statically in your configuration and place apply-time results only in
the map values.
Alternatively, you could use the -target
planning option to first apply only the resources that the for_each
value depends on, and then apply a second time to fully converge.
Luckily Terraform has a quite helpful error message, which tells us how we can work around the problem. Using the -target parameter we can first create the App Service like this
terraform apply -target=azurerm_linux_web_app.api_app
This should only create the App Service and dependencies required by it. Afterward, we can then execute Terraform normally and it should work as desired without any errors. It's not very pretty, but currently, there are no better ways of achieving exactly what you want.

Deploying and configuring Postgres server in Azure with Terraform

I'm deploying Azure Postgres Flexible Server with Terraform as described in GitHub. The configuration works as expected, no issues there. The only divination from that GitHub template is that I want to configure pgBouncer for Postgres which is now supported natively. I don't see a way how I can create this configuration (i.e., enable this feature).
I've done some research and discovered the configuration feature is not quite available (at least according to the open ticket in GitHub here). At the same time, one of the published replies suggests using azurerm_postgresql_flexible_server_configuration and this resource type is available in Terraform indeed. However, Microsoft documentation states that to enable and configure pgBouncer I need to introduce 7 parameters. I thought that to make the code tidier, I can use a list and foreach loop, like this:
locals {
pg_config = {
"pgbouncer.default_pool_size" : "50",
"pgBouncer.max_client_conn" : "5000",
"pgBouncer.pool_mode" : "TRANSACTION"
etc...
}
}
resource "azurerm_postgresql_flexible_server_configuration" "postgres_fs_config" {
for_each = local.pg_config
name = each.key
value = each.value
server_id = azurerm_postgresql_flexible_server.postgres-fs.id
}
Is this the best way to configure Postgres (without involving CDK)? Did I miss something?
Ok, I verified this approach and it works like a charm. Will stick to it for now.

Terraform - Azure - Extract API from one resource group and import into another resource group

I have 5 different APIs in my Dev environment. This environment was built manually.
However, for the subsequent environments like Test, Pre-Prod, etc.. Terraform is being used.
Since I need to create each of the APIs in the subsequent environments, am extracting each of these APIs as a JSON file, making minor tweaks to the API URLs and importing it into the new environments.
The following is the process that am doing right now.
Went to Resource groups in Azure
Then under API Management service > APIs, clicked on the necessary API
Now, clicked on the three dots next to the API that I need and clicked on Export
Selected OpenAPI v3 (JSON) format
Now, I'm using the extracted JSON file and using the Terraform code below to add it to the APIM
resource "azurerm_api_management_api" "example" {
name = "example-api"
resource_group_name = azurerm_resource_group.example.name
api_management_name = azurerm_api_management.example.name
revision = "1"
display_name = "Example API"
path = "api/path"
protocols = ["https"]
service_url = "https://actualURL-of-the-API"
import {
content_format = "openapi+json"
content_value = file("extracted-filename.json")
}
}
The issue here is:
Even though the API gets added to the APIM, this doesn't create all the data - like Webservice URL, Backend HTTP(s) endpoint
How do I go about doing this?
Are you locked into exporting the Json file and importing it on the other environments through Terraform?
The reason I ask is because I attempted something similar but decided to go another route.
Initially I created the API manually in a Dev environment. I then re-created the same API from the ground up using only Terraform. No Json export & import.
I then used that Terraform script to create my other environments.
That allowed me to bypass the import problem altogether since nothing is imported.
I have found that there are downsides to taking this approach; It is much less intuitive to author the API through the Terraform script than through the Azure GUI. This is therefore more time consuming. Especially since my initial API was discarded for the one generated with the Terraform script.
Additionally, I have had problems with Terraform diffs reporting example changes when there are none (I suspect the same problem is to be had when using the import method).
If you are wondering why I decided to go another route? The reason was twofold; Firstly, similar to you, I had trouble with getting the export/import to generate the API that I wanted. Secondly, I prefer not to rely on auto generated files.

Terraform provisioner trigger only for new instances / only run once

I have conditional provision steps I want to run for all compute instances created, but only run once.
I know I can put the provisioning within the compute resource, but then it cannot be conditional.
If I put it in a null_resource, I need a trigger, and I don't know how to trigger on only the newly created resources (i.e. if I already have 1 instance, and want to scale to 2, I want to only run provisioning on the 2nd being created, not run again on the 1st which is already provisioned).
How can I get a variable that only gives me the id or ip of the instance just created, as opposed to all of them?
Below an example of the provisioner.
resource "null_resource" "provisioning" {
count = var.condition ? length(var.instance_ips) : 0
triggers = {
instance_ids = join(",", var.instance_ips)
}
connection {
agent = false
timeout = "4m"
host = var.instance_ips[count.index]
user = "user"
private_key = var.ssh_private_key
}
provisioner "remote-exec" {
inline = [ do something, then remove the public key from authorized_keys ]
}
}
PS: the reason I only can run once (as opposed to run again and do nothing if already provisioned) is that I want to destroy the provisioning public key after I'm done, since it is using a tf generated key pair and the private key ends up in the state file, I want to make sure someone who gets access to the key pair still cannot access the instance.
Once the public key is removed from the authorized_keys the provisioner running a second time will just fail to connect, timeout and fail.
I found that I can use the on_failure: continue key, but then if it actual fails for legitimate reasons it would continue too.
I also could use a key pair that is generated locally with a local-exec provisioner so it doesn't show in the state file, but then the key is a file, which is not much different if someone get access to it; the file needs to stay on the machine, which may not work well with a cloud resource manager env that is recreated on a need to run basis.
And then I'm sure there are other ways to provision a file or script, but in this case it contains instance dependency data generated by TF, that I don't want left in a cloud-init.
So, I come down to needing to figure a way to use a trigger that only contains the new instance(s)
Any ideas how to do this?
https://www.terraform.io/docs/provisioners/
This documentation lists provisioners as a last resource and provides some suggestions on how to avoid having to use it, for various common resources.
Execute the script from the user_data, which is specifically designed for provisional, run-once actions. Since defining the user_data supports all regular Terraform interpolation, you can use that opportunity to pass environment variables or selectively include/exclude parts of a script, if you need conditional logic.
The downside is that any change in user_data results in recreating the instances, or creating a new launch configuration/template.

Terraform Variable Not Available

I am writing a terraform module to create a launch configuration along with ASG. It will create a "aws_key_pair" only if the keyname is not passed to the module for the ec2 instance. if the keyname is passed then use that keyname instead. I am doing it using the count function.
This all works fine. But when I pass the key to the module and try to use it in the launch configuration it is giving me the error that "aws_key_pair.key_pair.id" does not exist, which in a way is correct because it was not created since I passed the key. But somehow terraform is trying to look for the "aws_key_pair.key_pair.id" variable which does not exist.
Seems like a terraform problem in variable handling. I had the same problem with subnet but I was able to use it by using the coalesce function but this does not seems to work in case of key:
key_name = "${var.my_key == "" ?aws_key_pair.key_pair.id :var.my_key}"
This works for security groups:
security_groups = ["${coalesce(var.security_group,join("",aws_security_group.security_group.*.id))}"]
Any help is appreciated.

Resources