From 848ef5c2ffc93927c4e9ec7b01681d995900343d Mon Sep 17 00:00:00 2001 From: Kavithran <104263022+cavinkavi@users.noreply.github.com> Date: Thu, 19 Dec 2024 20:19:43 +0800 Subject: [PATCH] Add Azure Functions deployment implementation --- .../azurefunctionsconfig.yaml | 6 + azurefunctions_setup/host.json | 15 + azurefunctions_setup/local.settings.json | 8 + .../shared_azure_workload/azureworkload.py | 71 ++++ .../shared_azure_workload/function.json | 18 + .../shared_azure_workload/requirements.txt | 1 + cmd/config_azure_trace.json | 28 ++ cmd/loader.go | 3 +- go.mod | 1 + go.sum | 2 + pkg/driver/deployment/azure_functions.go | 321 ++++++++++++++++++ pkg/driver/deployment/deployer.go | 2 + 12 files changed, 475 insertions(+), 1 deletion(-) create mode 100644 azurefunctions_setup/azurefunctionsconfig.yaml create mode 100644 azurefunctions_setup/host.json create mode 100644 azurefunctions_setup/local.settings.json create mode 100644 azurefunctions_setup/shared_azure_workload/azureworkload.py create mode 100644 azurefunctions_setup/shared_azure_workload/function.json create mode 100644 azurefunctions_setup/shared_azure_workload/requirements.txt create mode 100644 cmd/config_azure_trace.json create mode 100644 pkg/driver/deployment/azure_functions.go diff --git a/azurefunctions_setup/azurefunctionsconfig.yaml b/azurefunctions_setup/azurefunctionsconfig.yaml new file mode 100644 index 000000000..78e36195e --- /dev/null +++ b/azurefunctions_setup/azurefunctionsconfig.yaml @@ -0,0 +1,6 @@ +# azurefunctionsconfig.yaml +azurefunctionsconfig: + resource_group: ExperimentResourceGroup # Name of the resource group + storage_account_name: invitrostorage # Name of the storage account + function_app_name: invitrofunctionapp # Name of the function app + location: EastUS # Region where the resources will be created diff --git a/azurefunctions_setup/host.json b/azurefunctions_setup/host.json new file mode 100644 index 000000000..4766572e7 --- /dev/null +++ b/azurefunctions_setup/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} diff --git a/azurefunctions_setup/local.settings.json b/azurefunctions_setup/local.settings.json new file mode 100644 index 000000000..19ed409f0 --- /dev/null +++ b/azurefunctions_setup/local.settings.json @@ -0,0 +1,8 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "python", + "AzureWebJobsFeatureFlags": "EnableWorkerIndexing", + "AzureWebJobsStorage": "" + } +} \ No newline at end of file diff --git a/azurefunctions_setup/shared_azure_workload/azureworkload.py b/azurefunctions_setup/shared_azure_workload/azureworkload.py new file mode 100644 index 000000000..db1b71e9a --- /dev/null +++ b/azurefunctions_setup/shared_azure_workload/azureworkload.py @@ -0,0 +1,71 @@ +import time +import os +import socket +import json +from typing import Dict + +# Global variables for IterationsMultiplier and hostname +IterationsMultiplier = 102 # Assuming similar cloud benchmark setup +hostname = socket.gethostname() + +# Simulate the busySpin function +def busy_spin(runtime_milli: int): + total_iterations = IterationsMultiplier * runtime_milli + for _ in range(total_iterations): + # Simulate a math-heavy operation + sqrt_of_10 = 10 ** 0.5 + +# Convert TraceFunctionExecution +def trace_function_execution(start: float, time_left_milliseconds: int) -> str: + time_consumed_milliseconds = int((time.time() - start) * 1000) + if time_consumed_milliseconds < time_left_milliseconds: + time_left_milliseconds -= time_consumed_milliseconds + if time_left_milliseconds > 0: + busy_spin(time_left_milliseconds) + + return f"OK - {hostname}" + +# The handler function for Azure Functions (Python) +import azure.functions as func +import logging + +def main(req: func.HttpRequest) -> func.HttpResponse: + logging.info("Processing request.") + + start_time = time.time() + + # Parse JSON request body + try: + req_body = req.get_json() + logging.info(f"Request body: {req_body}") + except ValueError: + logging.error("Invalid JSON received.") + return func.HttpResponse( + json.dumps({"error": "Invalid JSON"}), + status_code=400, + mimetype="application/json" + ) + + runtime_milliseconds = req_body.get('RuntimeInMilliSec', 1000) + memory_mebibytes = req_body.get('MemoryInMebiBytes', 128) + + logging.info(f"Runtime requested: {runtime_milliseconds} ms, Memory: {memory_mebibytes} MiB") + + # Trace the function execution (busy work simulation) + result_msg = trace_function_execution(start_time, runtime_milliseconds) + + # Prepare the response + response = { + "DurationInMicroSec": int((time.time() - start_time) * 1_000_000), + "MemoryUsageInKb": memory_mebibytes * 1024, + "Message": result_msg + } + + logging.info(f"Response: {response}") + + return func.HttpResponse( + json.dumps(response), + status_code=200, + mimetype="application/json" + ) + diff --git a/azurefunctions_setup/shared_azure_workload/function.json b/azurefunctions_setup/shared_azure_workload/function.json new file mode 100644 index 000000000..b1bc59b77 --- /dev/null +++ b/azurefunctions_setup/shared_azure_workload/function.json @@ -0,0 +1,18 @@ +{ + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": ["post"] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ], + "scriptFile": "azureworkload.py" +} + \ No newline at end of file diff --git a/azurefunctions_setup/shared_azure_workload/requirements.txt b/azurefunctions_setup/shared_azure_workload/requirements.txt new file mode 100644 index 000000000..f86a15a6a --- /dev/null +++ b/azurefunctions_setup/shared_azure_workload/requirements.txt @@ -0,0 +1 @@ +azure-functions \ No newline at end of file diff --git a/cmd/config_azure_trace.json b/cmd/config_azure_trace.json new file mode 100644 index 000000000..e6c0744e4 --- /dev/null +++ b/cmd/config_azure_trace.json @@ -0,0 +1,28 @@ +{ + "Seed": 42, + + "Platform": "AzureFunctions", + "InvokeProtocol" : "http1", + "EndpointPort": 80, + + "BusyLoopOnSandboxStartup": false, + + "TracePath": "data/traces/example", + "Granularity": "minute", + "OutputPathPrefix": "data/out/experiment", + "IATDistribution": "exponential", + "CPULimit": "1vCPU", + "ExperimentDuration": 15, + "WarmupDuration": 0, + + "IsPartiallyPanic": false, + "EnableZipkinTracing": false, + "EnableMetricsScrapping": false, + "MetricScrapingPeriodSeconds": 15, + "AutoscalingMetric": "concurrency", + + "GRPCConnectionTimeoutSeconds": 15, + "GRPCFunctionTimeoutSeconds": 900, + + "DAGMode": false +} \ No newline at end of file diff --git a/cmd/loader.go b/cmd/loader.go index bb69b80c5..d9dd21e9c 100644 --- a/cmd/loader.go +++ b/cmd/loader.go @@ -98,6 +98,7 @@ func main() { "Dirigent-RPS", "Dirigent-Dandelion-RPS", "Dirigent-Dandelion", + "AzureFunctions", } if !slices.Contains(supportedPlatforms, cfg.Platform) { @@ -150,7 +151,7 @@ func parseYAMLSpecification(cfg *config.LoaderConfiguration) string { case "firecracker": return "workloads/firecracker/trace_func_go.yaml" default: - if cfg.Platform != "Dirigent" && cfg.Platform != "Dirigent-RPS" && cfg.Platform != "Dirigent-Dandelion-RPS" && cfg.Platform != "Dirigent-Dandelion" { + if cfg.Platform != "Dirigent" && cfg.Platform != "Dirigent-RPS" && cfg.Platform != "Dirigent-Dandelion-RPS" && cfg.Platform != "Dirigent-Dandelion" && cfg.Platform != "AzureFunctions" { log.Fatal("Invalid 'YAMLSelector' parameter.") } } diff --git a/go.mod b/go.mod index ff3529550..38950a032 100644 --- a/go.mod +++ b/go.mod @@ -57,4 +57,5 @@ require ( golang.org/x/net v0.31.0 golang.org/x/text v0.20.0 // indirect google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index e8f3ad0d7..8aa7613ac 100644 --- a/go.sum +++ b/go.sum @@ -136,6 +136,8 @@ google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWn gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/driver/deployment/azure_functions.go b/pkg/driver/deployment/azure_functions.go new file mode 100644 index 000000000..ea3c27ff4 --- /dev/null +++ b/pkg/driver/deployment/azure_functions.go @@ -0,0 +1,321 @@ +package deployment + +import ( + "fmt" + "io" + "os" + "os/exec" + + "path/filepath" + + log "github.com/sirupsen/logrus" + "github.com/vhive-serverless/loader/pkg/common" + "github.com/vhive-serverless/loader/pkg/config" + "gopkg.in/yaml.v2" +) + +// Config struct to hold Azure Function deployment configuration +type Config struct { + AzureConfig struct { + ResourceGroup string `yaml:"resource_group"` + StorageAccountName string `yaml:"storage_account_name"` + FunctionAppName string `yaml:"function_app_name"` + Location string `yaml:"location"` + } `yaml:"azurefunctionsconfig"` +} + +type azureFunctionsDeployer struct { + functions []*common.Function +} + +func newAzureFunctionsDeployer() *azureFunctionsDeployer { + return &azureFunctionsDeployer{} +} + +func (afd *azureFunctionsDeployer) Deploy(cfg *config.Configuration) { + afd.functions = cfg.Functions + deployAzureFunctions(afd.functions) +} + +func (afd *azureFunctionsDeployer) Clean() { + cleanAzureFunctions(afd.functions) +} + +func deployAzureFunctions(functions []*common.Function) { + + // 1. Initialize resources required for Azure Functions deployment + // 2. Create function folders + // 3. Zip function folders + // 4. Deploy the function to Azure Functions + + // Load azurefunctionsconfig yaml file + config, err := LoadConfig("azurefunctions_setup/azurefunctionsconfig.yaml") + if err != nil { + log.Fatalf("Error loading azure functions config yaml: %s", err) + } + + baseDir := "azure_functions_for_zip" + + // 1. Initialize resources required for Azure Functions deployment + initAzureFunctions(config) + + // 2. Create function folders + if err := createFunctionFolders(baseDir, functions); err != nil { + log.Fatalf("Error setting up function folders required for zipping: %s", err) + } + + // 3. Zip function folders + if err := ZipFunctionAppFiles(); err != nil { + log.Fatalf("Error zipping function app files for deployment: %s", err) + } + + // 4. Deploy the function to Azure Functions + if err := DeployFunction(config, functions); err != nil { + log.Fatalf("Error deploying function: %s", err) + } + +} + +func cleanAzureFunctions(functions []*common.Function) { + + //Delete created folders + //Delete zip file + //Delete Azure resources + +} + +/* Functions for initializing resources required for Azure Functions deployment */ + +func initAzureFunctions(config *Config) { + + // 1.Create Resource Group + // 2.Create Storage Account + // 3.Create Function App + // 4.Set WEBSITE_RUN_FROM_PACKAGE + + //checkDependencies() ToDo: Check if all required dependencies are installed on VM + + // 1. Create Resource Group + if err := CreateResourceGroup(config); err != nil { + log.Fatalf("Error during Resource Group creation: %s", err) + } + + // 2. Create Storage Account + if err := CreateStorageAccount(config); err != nil { + log.Fatalf("Error during Storage Account creation: %s", err) + } + + // 3. Create Function App + if err := CreateFunctionApp(config); err != nil { + log.Fatalf("Error during Function App creation: %s", err) + } + + // 4. Set WEBSITE_RUN_FROM_PACKAGE + if err := SetWebsiteRunFromPackage(config); err != nil { + log.Fatalf("Error setting WEBSITE_RUN_FROM_PACKAGE: %s", err) + } + + log.Info("Azure Functions environment for deployment initialized successfully.") +} + +// LoadConfig reads the YAML configuration file +func LoadConfig(filePath string) (*Config, error) { + config := &Config{} + data, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + err = yaml.Unmarshal(data, config) + return config, err +} + +// CreateResourceGroup creates an Azure Resource Group: {az group create --name --location } +func CreateResourceGroup(config *Config) error { + createResourceGroupCmd := exec.Command("az", "group", "create", + "--name", config.AzureConfig.ResourceGroup, + "--location", config.AzureConfig.Location) + + if err := createResourceGroupCmd.Run(); err != nil { + return fmt.Errorf("failed to create resource group: %w", err) + } + + log.Infof("Resource group %s created successfully.", config.AzureConfig.ResourceGroup) + return nil +} + +// CreateStorageAccount creates an Azure Storage Account : {az storage account create --name --resource-group --location --sku Standard_LRS} +func CreateStorageAccount(config *Config) error { + cmd := exec.Command("az", "storage", "account", "create", + "--name", config.AzureConfig.StorageAccountName, + "--resource-group", config.AzureConfig.ResourceGroup, + "--location", config.AzureConfig.Location, + "--sku", "Standard_LRS") + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to create storage account: %w", err) + } + + log.Infof("Storage account %s created successfully.", config.AzureConfig.StorageAccountName) + return nil +} + +// CreateFunctionApp creates an Azure Function App: {az functionapp create --name --resource-group --storage-account --consumption-plan-location --runtime python --runtime-version 3.10 --os-type linux --functions-version 4} +func CreateFunctionApp(config *Config) error { + cmd := exec.Command("az", "functionapp", "create", + "--name", config.AzureConfig.FunctionAppName, + "--resource-group", config.AzureConfig.ResourceGroup, + "--storage-account", config.AzureConfig.StorageAccountName, + "--consumption-plan-location", config.AzureConfig.Location, + "--runtime", "python", + "--runtime-version", "3.10", + "--os-type", "linux", + "--functions-version", "4") + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to create function app: %w", err) + } + + log.Infof("Function app %s created successfully.", config.AzureConfig.FunctionAppName) + return nil +} + +// SetWebsiteRunFromPackage configures the function app to run from a zip package +func SetWebsiteRunFromPackage(config *Config) error { + cmd := exec.Command("az", "functionapp", "config", "appsettings", "set", + "--name", config.AzureConfig.FunctionAppName, + "--resource-group", config.AzureConfig.ResourceGroup, + "--settings", "WEBSITE_RUN_FROM_PACKAGE=1") + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to set WEBSITE_RUN_FROM_PACKAGE: %w", err) + } + + log.Info("WEBSITE_RUN_FROM_PACKAGE set successfully.") + return nil +} + +/* Functions for creating function folders before zipping */ + +// Function to create folders and copy files to the folders +func createFunctionFolders(baseDir string, function []*common.Function) error { + + for i := 0; i < len(function); i++ { + folderName := fmt.Sprintf("function%d", i) + folderPath := filepath.Join(baseDir, folderName) + + // Create the function folder + if err := os.MkdirAll(folderPath, os.ModePerm); err != nil { + return fmt.Errorf("failed to create folder %s: %w", folderPath, err) + } + + // Copy azureworkload.py, requirements.txt, and function.json into each function folder + if err := copyFile("azurefunctions_setup/shared_azure_workload/azureworkload.py", filepath.Join(folderPath, "azureworkload.py")); err != nil { + return fmt.Errorf("failed to copy azureworkload.py to %s: %w", folderPath, err) + } + if err := copyFile("azurefunctions_setup/shared_azure_workload/requirements.txt", filepath.Join(folderPath, "requirements.txt")); err != nil { + return fmt.Errorf("failed to copy requirements.txt to %s: %w", folderPath, err) + } + if err := copyFile("azurefunctions_setup/shared_azure_workload/function.json", filepath.Join(folderPath, "function.json")); err != nil { + return fmt.Errorf("failed to copy function.json to %s: %w", folderPath, err) + } + } + + log.Debugf("Created %d function folders with copies of azureworkload.py, requirements.txt, and function.json under %s folder.\n", len(function), baseDir) + return nil +} + +// Helper function to copy files +func copyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + destFile, err := os.Create(dst) + if err != nil { + return err + } + defer destFile.Close() + + _, err = io.Copy(destFile, sourceFile) + if err != nil { + return err + } + + return destFile.Sync() +} + +/* Functions for zipping created function folders */ + +func ZipFunctionAppFiles() error { + + // Use bash to zip the contents of azure_functions_for_zip/* along with host.json directly into azurefunctions.zip + cmd := exec.Command("bash", "-c", "cd azure_functions_for_zip && zip -r ../azurefunctions.zip . && cd .. && zip -j azurefunctions.zip azurefunctions_setup/host.json") + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to zip function app files for deployment: %w", err) + } + log.Info("Functions for deployment zipped successfully.") + return nil +} + +/* Functions for deploying zipped functions */ + +func DeployFunction(config *Config, function []*common.Function) error { + + log.Infof("Deploying %d functions to Azure Function App...", len(function)) + + // Path to the zip file that contains the Python binary and other resources for deployment to Azure + zipFilePath := "azurefunctions.zip" + + // Deploy the zip file to Azure Function App using CLI + cmd := exec.Command("az", "functionapp", "deployment", "source", "config-zip", + "--name", config.AzureConfig.FunctionAppName, + "--resource-group", config.AzureConfig.ResourceGroup, + "--src", zipFilePath) + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to deploy zip file to function app: %w", err) + } + + log.Infof("Deployed all %d functions successfully.", len(function)) + + // Storing endpoint for each function + for i := 0; i < len(function); i++ { + function[i].Endpoint = fmt.Sprintf("https://%s.azurewebsites.net/api/function%d", config.AzureConfig.FunctionAppName, i) + log.Infof("Function %s set to %s", function[i].Name, function[i].Endpoint) + } + + // Call the cleanup function after deployment, to delete temp folders and files + if err := cleanUpDeploymentFiles("azure_functions_for_zip", "azurefunctions.zip"); err != nil { + log.Errorf("Error during cleanup: %s", err) + } else { + log.Info("Deployment and cleanup of zip files completed successfully.") + } + + //Stop the program after deployment for testing purposes, to remove after invocation is implemented + log.Info("Stopping program after deployment phase for PR.") + os.Exit(0) // Exit with status code 0 (successful execution) + + return nil + +} + +/* Functions for clean up */ + +// Clean up temporary files and folders after deployment +func cleanUpDeploymentFiles(baseDir string, zipFile string) error { + // Remove the base directory containing function folders + if err := os.RemoveAll(baseDir); err != nil { + return fmt.Errorf("failed to remove directory %s: %w", baseDir, err) + } + log.Debugf("Successfully removed directory: %s", baseDir) + + // Remove the zip file used for deployment + if err := os.Remove(zipFile); err != nil { + return fmt.Errorf("failed to remove zip file %s: %w", zipFile, err) + } + log.Debugf("Successfully removed zip file: %s", zipFile) + + return nil +} diff --git a/pkg/driver/deployment/deployer.go b/pkg/driver/deployment/deployer.go index f82d952d2..d899873d9 100644 --- a/pkg/driver/deployment/deployer.go +++ b/pkg/driver/deployment/deployer.go @@ -14,6 +14,8 @@ func CreateDeployer(cfg *config.Configuration) FunctionDeployer { switch cfg.LoaderConfiguration.Platform { case "AWSLambda", "AWSLambda-RPS": return newAWSLambdaDeployer() + case "AzureFunctions": + return newAzureFunctionsDeployer() case "Dirigent", "Dirigent-RPS", "Dirigent-Dandelion", "Dirigent-Dandelion-RPS": return newDirigentDeployer() case "Knative", "Knative-RPS":