In the first two parts of the series, I introduced the architecture for building a link sharing relay and then built it with a little bit of F# code. This post covers setting up the infrastructure and deploying the code as an Azure Function.
Azure functions offer a really nice way to build serverless endpoints. Azure functions are phenomenally flexible, and in the previous part I showed how to set up an F# Azure function with an HttpTrigger
. It runs locally, but to make it useful we need to deploy it to Azure. We’ll need to configure not a small amount of infrastructure to do so.
Posts without posting, a series
This post will be a little lengthy, but hopefully not as long as the last. I’ll use terraform to set things up, but there’s one step that doesn’t seem to be working. Configuring a Custom Domain for the Azure Function seems not to work, so this will be a small manual step. It’s not ideal, but it’s only one extra step for managing DNS. Some of my terraform scripts are inherited from previous work, but I’ll do my best to present them here as if I am using them for the first time.
To deploy the function, we’ll do the following:
Let’s get started!
First, we’ll set up a couple variables and data source in terraform.
# variables.tf
variable "domain" {
type = string
default = "emilygorcenski.com"
}
variable "regions" {
type = map(string)
default = {
"primary" = "germanywestcentral"
"functions-eu" = "northeurope"
}
}
Not every region allows you to deploy Azure functions with a consumption plan (serverless), so I create some options in my variables. Next, we’ll set up our terraform provider. I’m ignoring the part where I have a remote terraform backend, but I recommend adding that, as well. My versions here are almost surely out of date, but I didn’t have time to fix that before writing this post. I’m on a schedule here!
# main.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 2.91"
}
azuread = {
source = "hashicorp/azuread"
version = "~> 2.0.0"
}
}
}
Finally, we can add the following to our main.tf
file. Of course, variable names in ALL_CAPS
should be replaced with something that makes sense for you.
resource "azurerm_resource_group" "rg" {
name = "RESOURCE_GROUP_NAME"
location = var.regions["primary"]
}
Once we have the resource group set up, we can configure a storage bucket that we can deploy our code into. Let’s create a new file, link-sharer.tf
, and put the following block in it.
resource "azurerm_storage_account" "links_app_storage" {
name = "STORAGE_NAME"
resource_group_name = azurerm_resource_group.rg.name
location = var.regions["functions-eu"]
account_tier = "Standard"
account_replication_type = "LRS"
}
No further action is needed here.
We’ll want to monitor our function, and while this step might be a little out of order, that’s ok. Let’s configure Azure Insights to let us monitor our function once we deploy. In link-sharer.tf
, add:
resource "azurerm_application_insights" "links_afapp_insights" {
name = "APP_INSIGHTS_NAME"
location = var.regions["functions-eu"]
resource_group_name = azurerm_resource_group.rg.name
application_type = "web"
}
This step is a little more difficult. Let’s create a new file, key-vault.tf
. We’ll start by configuring the Key Vault.
resource "azurerm_key_vault" "links_afapp_keyvault" {
name = "KEYVAULT_NAME"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
soft_delete_retention_days = 7
access_policy {
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = data.external.account_info.result.object_id
certificate_permissions = [
"Create",
"Delete",
"DeleteIssuers",
"Get",
"GetIssuers",
"Import",
"List",
"ListIssuers",
"ManageContacts",
"ManageIssuers",
"Purge",
"SetIssuers",
"Update"
]
key_permissions = [
"Backup",
"Create",
"Decrypt",
"Delete",
"Encrypt",
"Get",
"Import",
"List",
"Purge",
"Recover",
"Restore",
"Sign",
"UnwrapKey",
"Update",
"Verify",
"WrapKey"
]
secret_permissions = [
"Backup",
"Delete",
"Get",
"List",
"Purge",
"Recover",
"Restore",
"Set"
]
}
}
This will allow you to set up your Key Vault and gives your user account full permissions to manage keys. I think this is probably suboptimal, but I haven’t really dove into finding a better way to do this yet.
Next, in your main.tf
file, put the following:
provider "azurerm" {
features {
key_vault {
purge_soft_delete_on_destroy = true
}
}
}
This is needed because as of December 2020, all Key Vaults need to have “Soft Delete on Destroy” enabled.
Also in main.tf
, let’s load our local data from our JSON for the secrets. I suppose we could also be reading these from environment variables, too, but this is how I do it.
locals {
twitter_data = jsondecode(file("PATH/TO/TWITTER/CREDS/twitter-api.json"))
mastodon_data = jsondecode(file("PATH/TO/MASTODON/CREDS/mastodon-api.json"))
google_data = jsondecode(file("PATH/TO/GOOGLE/CREDS/google-key.json"))
}
Now, we can use this data in our key-vault.tf
file to set up the secrets.
resource "azurerm_key_vault_secret" "google-sheets-key" {
name = "GOOGLE-SERVICE-ACCOUNT-CREDENTIAL"
value = base64encode(local.google_data.GOOGLE_SERVICE_ACCOUNT_CREDENTIAL)
key_vault_id = azurerm_key_vault.links_afapp_keyvault.id
}
resource "azurerm_key_vault_secret" "google-sheet-id" {
name = "GOOGLE-SHEET-ID"
value = local.google_data.GOOGLE_SHEET_ID
key_vault_id = azurerm_key_vault.links_afapp_keyvault.id
}
resource "azurerm_key_vault_secret" "twitter-consumer-key" {
name = "twitter-consumer-key"
value = local.twitter_data.TWITTER_CONSUMER_KEY
key_vault_id = azurerm_key_vault.links_afapp_keyvault.id
}
resource "azurerm_key_vault_secret" "twitter-consumer-key-secret" {
name = "twitter-consumer-key-secret"
value = local.twitter_data.TWITTER_CONSUMER_KEY_SECRET
key_vault_id = azurerm_key_vault.links_afapp_keyvault.id
}
resource "azurerm_key_vault_secret" "twitter-access-token" {
name = "twitter-access-token"
value = local.twitter_data.TWITTER_ACCESS_TOKEN
key_vault_id = azurerm_key_vault.links_afapp_keyvault.id
}
resource "azurerm_key_vault_secret" "twitter-access-token-secret" {
name = "twitter-access-token-secret"
value = local.twitter_data.TWITTER_ACCESS_TOKEN_SECRET
key_vault_id = azurerm_key_vault.links_afapp_keyvault.id
}
resource "azurerm_key_vault_secret" "mastodon-server" {
name = "mastodon-server"
value = local.mastodon_data.MASTODON_SERVER
key_vault_id = azurerm_key_vault.links_afapp_keyvault.id
}
resource "azurerm_key_vault_secret" "mastodon-access-token" {
name = "mastodon-access-token"
value = local.mastodon_data.MASTODON_ACCESS_TOKEN
key_vault_id = azurerm_key_vault.links_afapp_keyvault.id
}
That’s it for now. We’ll come back to this.
We need to set up a Service Plan where we specify our compute SKU and other details for the Azure function. This is easy. We want to use a Y1
SKU on a Windows
OS. This is a consumption (serverless) SKU where you pay for what you use.
resource "azurerm_service_plan" "links_afapp_service_plan" {
name = "SERVICE_PLAN_NAME"
resource_group_name = azurerm_resource_group.rg.name
location = var.regions["functions-eu"]
sku_name = "Y1"
os_type = "Windows"
}
With all this done, we can finally set up the Azure Function. In your main.tf
, add the following:
resource "azurerm_windows_function_app" "links_app" {
name = "AZURE_FUNCTION_NAME"
resource_group_name = azurerm_resource_group.rg.name
location = var.regions["functions-eu"]
service_plan_id = azurerm_service_plan.links_afapp_service_plan.id
storage_account_name = azurerm_storage_account.links_app_storage.name
storage_account_access_key = azurerm_storage_account.links_app_storage.primary_access_key
identity {
type = "SystemAssigned"
}
site_config {
use_32_bit_worker = false
}
app_settings = {
"FUNCTIONS_WORKER_RUNTIME" = "dotnet",
"AzureWebJobsDisableHomepage" = "true",
"GOOGLE_SERVICE_ACCOUNT_CREDENTIAL" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.google-sheets-key.id})",
"GOOGLE_SHEET_ID" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.google-sheet-id.id})",
"APPINSIGHTS_INSTRUMENTATIONKEY" = azurerm_application_insights.il_afapp_insights.instrumentation_key,
"TWITTER_CONSUMER_KEY" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.twitter-consumer-key.id})",
"TWITTER_CONSUMER_KEY_SECRET" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.twitter-consumer-key-secret.id})",
"TWITTER_ACCESS_TOKEN" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.twitter-access-token.id})",
"TWITTER_ACCESS_TOKEN_SECRET" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.twitter-access-token-secret.id})",
"MASTODON_SERVER" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.mastodon-server.id})",
"MASTODON_ACCESS_TOKEN" = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.mastodon-access-token.id})",
}
}
The app settings here are very important, this is how we load secrets in from the Azure Key Vault. This should automatically load in the secrets. However, in my experience, if I change a secret, it does not load the latest version. I’ll figure that out later.
Before we can make this work, however, we need to let the Function App read from the Key Vault.
In your main.tf
, add the following at the end.
resource "azurerm_key_vault_access_policy" "il-managed-id-policy-eu" {
key_vault_id = azurerm_key_vault.links_afapp_keyvault.id
tenant_id = azurerm_windows_function_app.links_app.identity.0.tenant_id
object_id = azurerm_windows_function_app.links_app.identity.0.principal_id
secret_permissions = [
"Get"
]
}
Sometimes, on re-running terraform, this does not stick. I don’t know why. Sometimes I have to manually find the Principal ID, which is under “Identity” in the left nav in the Function App, and then copy the GUID into the “Access Policies” section within the Key Vault.
I can’t terraform this, perhaps because of lack of supported functionality, but in the Function App you can configure a custom CNAME domain under “Custom Domains.” Simply copy the relevant information to your DNS provider.
If the terraform works, then go ahead and apply the changes to your cloud, and let’s get ready to deploy the app! This is very simple. Navigate to your folder and type func azure functionapp publish AZURE_FUNCTION_NAME
, replacing the last bit with your function name, of course.
You’ll want to go to “Configuration” in your Function App to see that secrets are being properly read. If so, great! Go ahead and send a cURL request and see how it works!
If it doesn’t, email me and I will try to help. I probably missed something or made a mistake, because what I present here is a bit modified from my own setup.
Thanks for following along. Next up, integrating with an iOS shortcut!
Posted: 31.12.2022
Built: 21.12.2024
Updated: 24.04.2023
Hash: c546076
Words: 1438
Estimated Reading Time: 8 minutes