From e548803cb66abadbe56608dc910f360716efbe52 Mon Sep 17 00:00:00 2001 From: Max Dymond Date: Thu, 2 Oct 2025 11:48:11 +0100 Subject: [PATCH 1/3] Switch to FlexConsumption plan for Function App Microsoft are deprecating the Consumption plan for Linux Function Apps, so we needed to switch over to FlexConsumption. Fixes #78 --- .flake8 | 34 --------- .github/workflows/ci.yml | 1 + CHANGELOG.md | 10 ++- README.md | 2 +- function_app.py | 8 ++- pyproject.toml | 4 +- rg.bicep | 6 +- rg_funcapp.bicep | 52 +++++++++----- ruff.toml | 3 +- src/apt_package_function/__init__.py | 2 +- src/apt_package_function/azcmd.py | 2 +- src/apt_package_function/bicep_deployment.py | 2 +- src/apt_package_function/create_resources.py | 15 ++-- src/apt_package_function/func_app.py | 72 ++++---------------- src/apt_package_function/poetry.py | 2 +- src/apt_package_function/resource_group.py | 2 +- 16 files changed, 83 insertions(+), 134 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index a447d39..0000000 --- a/.flake8 +++ /dev/null @@ -1,34 +0,0 @@ -[flake8] -max-line-length = 88 -extend-ignore = E203 -exclude = - .python_packages - .venv - .git - __pycache__ - .mypy_cache - -ignore = - # Default ignores - E121,E123,E126,E226,E24,E704,W503,W504 - - # Auto-fmt compatibility - # Whitespace before ':' | NOT PEP8 COMPLIANT - E203, - # Allow long lines if needed | BLACK HANDLES CODE FORMATTING - E501, - # Multi-line docstring summary should start at the first line - D212, - - # Allow looser flake8-docstrings / pydocstyle restrictions - # One-line docstring should fit on one line with quotes - D200, - # First line should end with a period - D400, - # First line should be in imperative mood - D401, - - # Subprocess module imported. Warning to be careful only. - S404, - # Subprocess used without shell=True. Warning to be careful only. - S603, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bea534c..944cd76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,3 +1,4 @@ +# Copyright (c) Alianza, Inc. All rights reserved. name: Lint on: [push] diff --git a/CHANGELOG.md b/CHANGELOG.md index 80aa57d..999c4ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ Status: Available for use ### Fixed +## [1.0.0] - 2025-10-02 + +### Breaking Changes +- Switch to using FlexConsumption as Consumption apps are being deprecated. + ## [0.1.0] - 2024-05-16 ### Added @@ -24,5 +29,6 @@ Status: Available for use ### Changed -[unreleased]: https://github.com/microsoft/apt-package-function/compare/0.1.0...HEAD -[0.1.0]: https://github.com/microsoft/apt-package-function/tree/0.1.0 +[unreleased]: https://github.com/Metaswitch/apt-package-function/compare/1.0.0...HEAD +[1.0.0]: https://github.com/Metaswitch/apt-package-function/compare/0.1.0...1.0.0 +[0.1.0]: https://github.com/Metaswitch/apt-package-function/tree/0.1.0 diff --git a/README.md b/README.md index 54b0f05..357d267 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Functionality to create a Debian package repository in Azure Blob Storage with an Azure Function App to keep it up to date. For use with -[apt-transport-blob](https://github.com/microsoft/apt-transport-blob). +[apt-transport-blob](https://github.com/Metaswitch/apt-transport-blob). # Getting Started diff --git a/function_app.py b/function_app.py index 3064f6a..22ff1b6 100644 --- a/function_app.py +++ b/function_app.py @@ -1,4 +1,4 @@ -# Copyright (c) Microsoft Corporation. +# Copyright (c) Alianza, Inc. All rights reserved. # Licensed under the MIT License. """A function app to manage a Debian repository in Azure Blob Storage.""" @@ -9,7 +9,7 @@ import os import tempfile from pathlib import Path -from typing import Generator +from typing import Generator, Optional import azure.functions as func import pydpkg @@ -32,12 +32,14 @@ @contextlib.contextmanager def temporary_filename() -> Generator[str, None, None]: """Create a temporary file and return the filename.""" + temporary_name: Optional[str] = None try: with tempfile.NamedTemporaryFile(delete=False) as f: temporary_name = f.name yield temporary_name finally: - os.unlink(temporary_name) + if temporary_name: + os.unlink(temporary_name) class PackageBlob: diff --git a/pyproject.toml b/pyproject.toml index 056293e..65ed014 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,10 @@ +# Copyright (c) Alianza, Inc. All rights reserved. +# Highly Confidential Material [project] name = "apt-package-function" description = "Functionality to create a Debian package repository in Azure Blob Storage" license = "MIT" -version = "0.1.0" +version = "1.0.0" readme = "README.md" authors = [{name = "Max Dymond", email = "max.dymond@alianza.com"}] requires-python = '>=3.9.2,<4.0.0' diff --git a/rg.bicep b/rg.bicep index 8a52ec9..8535d56 100644 --- a/rg.bicep +++ b/rg.bicep @@ -122,14 +122,14 @@ az storage blob upload --auth-mode login -f Packages -c "${AZURE_BLOB_CONTAINER} } } -// Create the function app directly, if shared key support is enabled -module funcapp 'rg_funcapp.bicep' = if (use_shared_keys) { +// Create the function app directly +module funcapp 'rg_funcapp.bicep' = { name: 'funcapp${suffix}' params: { location: location storage_account_name: storageAccount.name appName: appName - use_shared_keys: true + use_shared_keys: use_shared_keys suffix: suffix } } diff --git a/rg_funcapp.bicep b/rg_funcapp.bicep index 19b2ad2..bc0555e 100644 --- a/rg_funcapp.bicep +++ b/rg_funcapp.bicep @@ -65,12 +65,14 @@ resource storageBlobDataContributorRoleAssignment 'Microsoft.Authorization/roleA } // Create a hosting plan for the function app +// Using Flex Consumption plan for serverless hosting with enhanced features +// Reference: https://learn.microsoft.com/en-us/azure/azure-functions/flex-consumption-plan resource hostingPlan 'Microsoft.Web/serverfarms@2024-11-01' = { name: hostingPlanName location: location sku: { - name: 'Y1' - tier: 'Dynamic' + name: 'FC1' + tier: 'FlexConsumption' } properties: { reserved: true @@ -90,18 +92,10 @@ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { // Construct the app settings var common_settings = [ - { - name: 'FUNCTIONS_EXTENSION_VERSION' - value: '~4' - } { name: 'APPINSIGHTS_INSTRUMENTATIONKEY' value: applicationInsights.properties.InstrumentationKey } - { - name: 'FUNCTIONS_WORKER_RUNTIME' - value: 'python' - } // Pass the blob container name to the function app - this is the // container which is monitored for new packages. { @@ -129,17 +123,42 @@ var app_settings = use_shared_keys ? concat(common_settings, [ name: 'AzureWebJobsStorage__accountName' value: storageAccount.name } - { - name: 'WEBSITE_RUN_FROM_PACKAGE' - value: 'https://${storageAccount.name}.blob.${environment().suffixes.storage}/${pythonContainer.name}/function_app.zip' - } - // Pass the container URL to the function app for the `from_container_url` call. { name: 'BLOB_CONTAINER_URL' value: 'https://${storageAccount.name}.blob.${environment().suffixes.storage}/${packageContainer.name}/' } ]) +var function_runtime = { + name: 'python' + version: python_version +} + +var flex_deployment_configuration = { + storage: { + type: 'blobContainer' + value: 'https://${storageAccount.name}.blob.${environment().suffixes.storage}/${pythonContainer.name}' + authentication: { + type: 'SystemAssignedIdentity' + } + } +} + +var flex_scale_and_concurrency = { + maximumInstanceCount: 100 + instanceMemoryMB: 2048 +} + +var function_app_config = use_shared_keys ? { + runtime: function_runtime + scaleAndConcurrency: flex_scale_and_concurrency +} : union({ + runtime: function_runtime + scaleAndConcurrency: flex_scale_and_concurrency +}, { + deployment: flex_deployment_configuration +}) + // Create the function app. resource functionApp 'Microsoft.Web/sites@2024-11-01' = { name: functionAppName @@ -152,13 +171,12 @@ resource functionApp 'Microsoft.Web/sites@2024-11-01' = { properties: { serverFarmId: hostingPlan.id siteConfig: { - linuxFxVersion: 'Python|${python_version}' - pythonVersion: python_version appSettings: app_settings ftpsState: 'FtpsOnly' minTlsVersion: '1.2' } httpsOnly: true + functionAppConfig: function_app_config } } diff --git a/ruff.toml b/ruff.toml index 61fbc6c..a74fed5 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,3 +1,4 @@ +# Copyright (c) Alianza, Inc. All rights reserved. exclude = [ ".venv", "__pycache__", @@ -56,4 +57,4 @@ indent-style = "space" skip-magic-trailing-comma = false # Like Black, automatically detect the appropriate line ending. -line-ending = "auto" \ No newline at end of file +line-ending = "auto" diff --git a/src/apt_package_function/__init__.py b/src/apt_package_function/__init__.py index 18d8fdd..ae0c7cc 100644 --- a/src/apt_package_function/__init__.py +++ b/src/apt_package_function/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) Microsoft Corporation. +# Copyright (c) Alianza, Inc. All rights reserved. # Licensed under the MIT License. """Tooling for creating apt repositories in Azure.""" diff --git a/src/apt_package_function/azcmd.py b/src/apt_package_function/azcmd.py index a18f7e1..4701fb2 100644 --- a/src/apt_package_function/azcmd.py +++ b/src/apt_package_function/azcmd.py @@ -1,4 +1,4 @@ -# Copyright (c) Microsoft Corporation. +# Copyright (c) Alianza, Inc. All rights reserved. # Licensed under the MIT License. """Functions for interacting with az cli""" diff --git a/src/apt_package_function/bicep_deployment.py b/src/apt_package_function/bicep_deployment.py index 8001aaa..cb8b908 100644 --- a/src/apt_package_function/bicep_deployment.py +++ b/src/apt_package_function/bicep_deployment.py @@ -1,4 +1,4 @@ -# Copyright (c) Microsoft Corporation. +# Copyright (c) Alianza, Inc. All rights reserved. # Licensed under the MIT License. """Manages Bicep deployments.""" diff --git a/src/apt_package_function/create_resources.py b/src/apt_package_function/create_resources.py index 545c9d7..820acd3 100644 --- a/src/apt_package_function/create_resources.py +++ b/src/apt_package_function/create_resources.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) Microsoft Corporation. +# Copyright (c) Alianza, Inc. All rights reserved. # Licensed under the MIT License. """Creates resources for the apt package function in Azure.""" @@ -10,7 +10,11 @@ from apt_package_function import common_logging from apt_package_function.bicep_deployment import BicepDeployment -from apt_package_function.func_app import FuncApp, FuncAppBundle, FuncAppZip +from apt_package_function.func_app import ( + FuncApp, + FuncAppBundle, + FuncAppZip, +) from apt_package_function.poetry import extract_requirements from apt_package_function.resource_group import create_rg @@ -78,7 +82,6 @@ def main() -> None: apt_sources = outputs["apt_sources"] function_app_name = outputs["function_app_name"] package_container = outputs["package_container"] - python_container = outputs["python_container"] storage_account = outputs["storage_account"] # Create the function app @@ -87,11 +90,7 @@ def main() -> None: funcapp = FuncAppZip(name=function_app_name, resource_group=args.resource_group) else: funcapp = FuncAppBundle( - name=function_app_name, - resource_group=args.resource_group, - storage_account=storage_account, - python_container=python_container, - parameters=common_parameters, + name=function_app_name, resource_group=args.resource_group ) with funcapp as cm: diff --git a/src/apt_package_function/func_app.py b/src/apt_package_function/func_app.py index 0b0b735..f8add19 100644 --- a/src/apt_package_function/func_app.py +++ b/src/apt_package_function/func_app.py @@ -1,4 +1,4 @@ -# Copyright (c) Microsoft Corporation. +# Copyright (c) Alianza, Inc. All rights reserved. # Licensed under the MIT License. """Management of function applications""" @@ -10,11 +10,10 @@ from pathlib import Path from subprocess import CalledProcessError from types import TracebackType -from typing import Any, Dict, Optional, Type +from typing import Optional, Type from zipfile import ZipFile from apt_package_function.azcmd import AzCmdJson, AzCmdNone -from apt_package_function.bicep_deployment import BicepDeployment log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -128,34 +127,27 @@ def deploy(self) -> None: class FuncAppBundle(FuncApp): - """Bundles the function app using the core-tools tooling.""" + """Publishes the function app using the core-tools tooling.""" - def __init__( - self, - name: str, - resource_group: str, - storage_account: str, - python_container: str, - parameters: Dict[str, Any], - ) -> None: + def __init__(self, name: str, resource_group: str) -> None: """Create a FuncAppBundle object.""" - # The function app bundle gets created as function_app.zip super().__init__(name, resource_group, Path("function_app.zip")) - self.storage_account = storage_account - self.python_container = python_container - self.parameters = parameters + def deploy(self) -> None: + """Deploy the function application.""" + log.info("Deploying function app code") cwd = Path.cwd() + home = Path.home() + azure_config = home / ".azure" - # Pack the application using the core-tools tooling - # Should generate a file called function_app.zip + # Publish the application using the core-tools tooling cmd = [ "docker", "run", "-it", "--rm", "-v", - "/var/run/docker.sock:/var/run/docker.sock", + f"{azure_config}:/root/.azure", "-v", f"{cwd}:/function_app", "-w", @@ -163,46 +155,8 @@ def __init__( "mcr.microsoft.com/azure-functions/python:4-python3.11-core-tools", "bash", "-c", - "func pack --python --build-native-deps", + f"func azure functionapp publish {self.name} --python --build remote", ] log.debug("Running %s", cmd) subprocess.run(cmd, check=True) - - def deploy(self) -> None: - """Deploy the function application.""" - log.info("Copying function app code to %s", self.python_container) - cmd = AzCmdNone( - [ - "az", - "storage", - "blob", - "upload", - "--auth-mode", - "login", - "--account-name", - self.storage_account, - "--container-name", - self.python_container, - "--file", - str(self.output_path), - "--name", - str(self.output_path), - "--overwrite", - ] - ) - cmd.run() - - # Create the function app - func_app_params = { - "use_shared_keys": False, - } - func_app_params.update(self.parameters) - - func_app_deploy = BicepDeployment( - deployment_name=f"deploy_{self.name}", - resource_group_name=self.resource_group, - template_file=Path("rg_funcapp.bicep"), - parameters=func_app_params, - description="function app", - ) - func_app_deploy.create() + log.info("Function app code published to %s", self.name) diff --git a/src/apt_package_function/poetry.py b/src/apt_package_function/poetry.py index 3b5e92c..597b41c 100644 --- a/src/apt_package_function/poetry.py +++ b/src/apt_package_function/poetry.py @@ -1,4 +1,4 @@ -# Copyright (c) Microsoft Corporation. +# Copyright (c) Alianza, Inc. All rights reserved. # Licensed under the MIT License. """Extracts requirements from a poetry project.""" diff --git a/src/apt_package_function/resource_group.py b/src/apt_package_function/resource_group.py index 910f35c..f7c1b78 100644 --- a/src/apt_package_function/resource_group.py +++ b/src/apt_package_function/resource_group.py @@ -1,4 +1,4 @@ -# Copyright (c) Microsoft Corporation. +# Copyright (c) Alianza, Inc. All rights reserved. # Licensed under the MIT License. """Manages resource groups.""" From c9cd4ccc40d43ff484d3eb00b3864df5a2054e8c Mon Sep 17 00:00:00 2001 From: Max Dymond Date: Thu, 2 Oct 2025 11:59:36 +0100 Subject: [PATCH 2/3] Tidy up common config --- rg_funcapp.bicep | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rg_funcapp.bicep b/rg_funcapp.bicep index bc0555e..3750d8f 100644 --- a/rg_funcapp.bicep +++ b/rg_funcapp.bicep @@ -149,13 +149,14 @@ var flex_scale_and_concurrency = { instanceMemoryMB: 2048 } -var function_app_config = use_shared_keys ? { +// Define common app config +var common_app_config = { runtime: function_runtime scaleAndConcurrency: flex_scale_and_concurrency -} : union({ - runtime: function_runtime - scaleAndConcurrency: flex_scale_and_concurrency -}, { +} + +// For managed identity, add the deployment configuration, otherwise just use common config +var function_app_config = use_shared_keys ? common_app_config : union(common_app_config, { deployment: flex_deployment_configuration }) From 2708dfc6c5210b854668490a633566bc9ebf2a00 Mon Sep 17 00:00:00 2001 From: Max Dymond Date: Thu, 2 Oct 2025 12:19:44 +0100 Subject: [PATCH 3/3] Get shared-key version working as well --- rg.bicep | 4 ++-- rg_funcapp.bicep | 38 +++++++++++++++++++------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/rg.bicep b/rg.bicep index 8535d56..9c3cde1 100644 --- a/rg.bicep +++ b/rg.bicep @@ -63,7 +63,7 @@ resource packageContainer 'Microsoft.Storage/storageAccounts/blobServices/contai properties: { } } -resource pythonContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-01-01' = if (!use_shared_keys) { +resource pythonContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-01-01' = { parent: defBlobServices name: python_container_name properties: { @@ -139,4 +139,4 @@ output apt_sources string = 'deb [trusted=yes] blob://${storageAccount.name}.blo output function_app_name string = appName output storage_account string = storageAccount.name output package_container string = packageContainer.name -output python_container string = use_shared_keys ? '' : pythonContainer.name +output python_container string = pythonContainer.name diff --git a/rg_funcapp.bicep b/rg_funcapp.bicep index 3750d8f..093877a 100644 --- a/rg_funcapp.bicep +++ b/rg_funcapp.bicep @@ -27,6 +27,8 @@ var package_container_name = 'packages' // Create a container for the Python code var python_container_name = 'python' +var storage_connection_string = 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}' + // The version of Python to run with var python_version = '3.11' @@ -50,7 +52,7 @@ resource packageContainer 'Microsoft.Storage/storageAccounts/blobServices/contai parent: defBlobServices name: package_container_name } -resource pythonContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-01-01' existing = if (!use_shared_keys) { +resource pythonContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-01-01' existing = { parent: defBlobServices name: python_container_name } @@ -108,15 +110,11 @@ var common_settings = [ var app_settings = use_shared_keys ? concat(common_settings, [ { name: 'AzureWebJobsStorage' - value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}' - } - { - name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING' - value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${storageAccount.listKeys().keys[0].value}' + value: storage_connection_string } { - name: 'WEBSITE_CONTENTSHARE' - value: toLower(functionAppName) + name: 'DEPLOYMENT_STORAGE_CONNECTION_STRING' + value: storage_connection_string } ]) : concat(common_settings, [ { @@ -134,13 +132,20 @@ var function_runtime = { version: python_version } +var deployment_storage_value = 'https://${storageAccount.name}.blob.${environment().suffixes.storage}/${pythonContainer.name}' + +var deployment_authentication = use_shared_keys ? { + type: 'StorageAccountConnectionString' + storageAccountConnectionStringName: 'DEPLOYMENT_STORAGE_CONNECTION_STRING' +} : { + type: 'SystemAssignedIdentity' +} + var flex_deployment_configuration = { storage: { type: 'blobContainer' - value: 'https://${storageAccount.name}.blob.${environment().suffixes.storage}/${pythonContainer.name}' - authentication: { - type: 'SystemAssignedIdentity' - } + value: deployment_storage_value + authentication: deployment_authentication } } @@ -149,16 +154,11 @@ var flex_scale_and_concurrency = { instanceMemoryMB: 2048 } -// Define common app config -var common_app_config = { +var function_app_config = { runtime: function_runtime scaleAndConcurrency: flex_scale_and_concurrency -} - -// For managed identity, add the deployment configuration, otherwise just use common config -var function_app_config = use_shared_keys ? common_app_config : union(common_app_config, { deployment: flex_deployment_configuration -}) +} // Create the function app. resource functionApp 'Microsoft.Web/sites@2024-11-01' = {