Skip to content
Draft
1 change: 1 addition & 0 deletions function-app-v1/ProviderRelay/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def get_session(client_id, audience, role_arn):
managed_identity_client=client_id, exclude_environment_credential=True
)
token = creds.get_token(audience)
logging.info(f"token: {token}")
try:
res = client.assume_role_with_web_identity(
WebIdentityToken=token.token,
Expand Down
61 changes: 49 additions & 12 deletions function.tf
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ resource "azurerm_application_insights" "stacklet" {
resource_group_name = azurerm_resource_group.stacklet_rg.name
application_type = "web"
tags = local.tags
retention_in_days = 90
}

resource "azurerm_service_plan" "stacklet" {
Expand Down Expand Up @@ -84,38 +85,40 @@ data "archive_file" "function_app" {
output_path = "${path.module}/function-app.zip"
}

# azurerm_linux_function_app's zip_deploy_file will only redeploy the function
# when the path changes, so copy the zip to a versioned filename.
resource "local_file" "function_app_versioned" {
filename = "${path.module}/function-app-${data.archive_file.function_app.output_sha256}.zip"
source = "${path.module}/function-app.zip"
}

resource "azurerm_linux_function_app" "stacklet" {
name = "stacklet-${var.prefix}-function-app-${substr(random_string.storage_account_suffix.result, 0, 15)}"
name = "${var.prefix}-relay-app"
resource_group_name = azurerm_resource_group.stacklet_rg.name
location = azurerm_resource_group.stacklet_rg.location
service_plan_id = azurerm_service_plan.stacklet.id

storage_account_name = azurerm_storage_account.stacklet.name
storage_account_access_key = azurerm_storage_account.stacklet.primary_access_key
# storage_uses_managed_identity = true
# After the function has been deployed, this value can be turned on, and
# the storage_account_access_key removed. However, doing this also stops
# future redeployment of the function code from working as the azure cli
# looks for the `AzureWebJobsStorage` application setting. When using the
# managed identity, this value is not set, and instead
# `AzureWebJobsStorage__accountKey` is set. This causes the azure cli to
# fail to deploy the function code.

# Enforce HTTPS on the HTTP endpoint even though the data plane aspect of it
# is unused, to avoid showing up in security checks.
https_only = true

# Deploy from zip file
zip_deploy_file = local_file.function_app_versioned.filename
client_certificate_enabled = false
public_network_access_enabled = false

site_config {
application_insights_key = azurerm_application_insights.stacklet.instrumentation_key
application_insights_key = azurerm_application_insights.stacklet.instrumentation_key
application_insights_connection_string = azurerm_application_insights.stacklet.connection_string

application_stack {
python_version = "3.12"
}

# More somewhat pointless HTTP security; the HTTP data plane is unused.
minimum_tls_version = "1.3"
http2_enabled = true
}

app_settings = {
Expand Down Expand Up @@ -150,3 +153,37 @@ resource "azurerm_linux_function_app" "stacklet" {
]
}
}

# In order to get the underlying function in the application to be redeployed
# when the zip hash changes, we need use fork out to the azure cli.
# We also need to temporarily enable public access to allow the zip file to be deployed.
resource "null_resource" "deploy_function_app" {
depends_on = [azurerm_linux_function_app.stacklet]

provisioner "local-exec" {
command = <<-EOT
# Temporarily enable public access
az functionapp update \
--name ${azurerm_linux_function_app.stacklet.name} \
--resource-group ${azurerm_resource_group.stacklet_rg.name} \
--set publicNetworkAccess=Enabled

# Deploy the code
az functionapp deployment source config-zip \
--name ${azurerm_linux_function_app.stacklet.name} \
--resource-group ${azurerm_resource_group.stacklet_rg.name} \
--src ${data.archive_file.function_app.output_path} \
--build-remote true

# Disable public access again
az functionapp update \
--name ${azurerm_linux_function_app.stacklet.name} \
--resource-group ${azurerm_resource_group.stacklet_rg.name} \
--set publicNetworkAccess=Disabled
EOT
}

triggers = {
build_hash = data.archive_file.function_app.output_sha256
}
}
2 changes: 1 addition & 1 deletion locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
locals {
app_role_id = var.azuread_application == null ? random_uuid.app_role_uuid.id : data.azuread_application.stacklet_application[0].app_role_ids.AssumeRoleWithWebIdentity

audience = "api://stacklet/provider/azure/${var.aws_target_prefix}"
audience = "api://${data.azurerm_subscription.current.tenant_id}/stacklet/provider/azure/${var.aws_target_prefix}"

_tags = {
"stacklet:app" : "Azure Relay"
Expand Down
23 changes: 22 additions & 1 deletion main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ data "azurerm_role_definition" "builtin" {
resource "random_uuid" "app_role_uuid" {}

locals {
resource_group_name = coalesce(var.resource_group_name, var.prefix)
resource_group_name = coalesce(var.resource_group_name, "${var.prefix}-stacklet-relay")
}

resource "azurerm_resource_group" "stacklet_rg" {
Expand All @@ -44,6 +44,27 @@ resource "azurerm_user_assigned_identity" "stacklet_identity" {
tags = local.tags
}

# Role assignment for storage queue access
resource "azurerm_role_assignment" "storage_queue_data_contributor" {
scope = azurerm_storage_account.stacklet.id
role_definition_name = "Storage Queue Data Contributor"
principal_id = azurerm_user_assigned_identity.stacklet_identity.principal_id
}

# Role assignment for storage blob access (for function app packages)
resource "azurerm_role_assignment" "storage_blob_data_contributor" {
scope = azurerm_storage_account.stacklet.id
role_definition_name = "Storage Blob Data Contributor"
principal_id = azurerm_user_assigned_identity.stacklet_identity.principal_id
}

# Role assignment for Application Insights
resource "azurerm_role_assignment" "monitoring_contributor" {
scope = azurerm_application_insights.stacklet.id
role_definition_name = "Monitoring Contributor"
principal_id = azurerm_user_assigned_identity.stacklet_identity.principal_id
}

resource "azuread_application" "stacklet_application" {
count = var.azuread_application == null ? 1 : 0
display_name = "${var.prefix}-application"
Expand Down
3 changes: 3 additions & 0 deletions provider.tf
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,7 @@ provider "azurerm" {
}

subscription_id = var.subscription_id

# Use Azure AD authentication for storage operations (required when access keys are disabled)
storage_use_azuread = true
}
21 changes: 20 additions & 1 deletion storage.tf
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,26 @@ resource "azurerm_storage_account" "stacklet" {
location = azurerm_resource_group.stacklet_rg.location
account_tier = "Standard"
account_replication_type = "LRS"
tags = local.tags

# Defaults to https only, and min tls version is 1.2

# allow_nested_items_to_be_public = false
# Setting this value to false prevents the deployment of the function app
# using the azure cli.

# public_network_access_enabled = true
# Setting this value stops terraform being able to query the state of the
# storage account. It seems that the control plane and data plane are
# effectively configured together. In order to turn this value on, the
# storage account needs to configure either selective IP allow lists or
# selective VPCs.

# shared_access_key_enabled = false
# Turning this value on seems to prevent the servers running the function
# app from acquiring some form of internal lock that is necessary, so the
# azure function wrapping code is never able to query the storage queue.

tags = local.tags
}

resource "azurerm_storage_queue" "stacklet" {
Expand Down