-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Integrate Azure Functions deployment with In-Vitro #569
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
name: End-to-End Azure Functions Tests | ||
|
||
on: | ||
push: | ||
branches: | ||
- main # Trigger the workflow when code is pushed to the main branch | ||
pull_request: | ||
branches: | ||
- main # Trigger the workflow when the PR targets the main branch | ||
workflow_dispatch: # Allows manual triggering of the workflow | ||
|
||
env: | ||
GOOS: linux | ||
GO111MODULE: on | ||
|
||
jobs: | ||
test-azure: | ||
name: Test E2E Azure Functions Cloud Deployment | ||
runs-on: ubuntu-20.04 | ||
env: | ||
AZURE_APP_ID: ${{ secrets.AZURE_APP_ID }} | ||
AZURE_PASSWORD: ${{ secrets.AZURE_PASSWORD }} | ||
AZURE_TENANT: ${{ secrets.AZURE_TENANT }} | ||
|
||
steps: | ||
- name: Check if environment variables are set # Validate secrets are passed | ||
run: | | ||
if [[ -z "$AZURE_APP_ID" ]]; then | ||
echo "AZURE_APP_ID is not set. Please check if secrets.AZURE_APP_ID is in the repository." | ||
exit 1 | ||
fi | ||
if [[ -z "$AZURE_PASSWORD" ]]; then | ||
echo "AZURE_PASSWORD is not set. Please check if secrets.AZURE_PASSWORD is in the repository." | ||
exit 1 | ||
fi | ||
if [[ -z "$AZURE_TENANT" ]]; then | ||
echo "AZURE_TENANT is not set. Please check if secrets.AZURE_TENANT is in the repository." | ||
exit 1 | ||
fi | ||
|
||
- name: Checkout GitHub Repository | ||
uses: actions/checkout@v4 | ||
with: | ||
lfs: true | ||
|
||
- name: Install Azure CLI | ||
run: | | ||
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash | ||
az --version | ||
|
||
- name: Install Golang | ||
uses: actions/setup-go@v5 | ||
with: | ||
go-version: 1.22 | ||
|
||
- name: Set up Python 3.10 | ||
uses: actions/setup-python@v4 | ||
with: | ||
python-version: '3.10' | ||
|
||
- name: Azure CLI Login Using Service Principal | ||
run: az login --service-principal --username $AZURE_APP_ID --password $AZURE_PASSWORD --tenant $AZURE_TENANT | ||
|
||
- name: Build and Run Loader | ||
run: go run cmd/loader.go --config cmd/config_azure_trace.json | ||
|
||
- name: Check the output | ||
run: test -f "data/out/experiment_duration_5.csv" && test $(grep true data/out/experiment_duration_5.csv | wc -l) -eq 0 # test the output file for errors (true means failure to invoke) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
name: Unit Tests for Azure Functions | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
branches: | ||
- main | ||
workflow_dispatch: | ||
|
||
jobs: | ||
test: | ||
name: Run Azure Functions Unit Tests | ||
runs-on: ubuntu-20.04 | ||
|
||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v4 | ||
|
||
- name: Set up Go | ||
uses: actions/setup-go@v5 | ||
with: | ||
go-version: '1.22' | ||
|
||
- name: Run Workload Copy Test | ||
run: go test -v ./pkg/driver/deployment/azure_functions_test.go -run TestCopyPythonWorkload | ||
|
||
- name: Run Zip File Health Test | ||
run: go test -v ./pkg/driver/deployment/azure_functions_test.go -run TestZipHealth | ||
|
||
- name: Run Deploy Function Test | ||
run: go test -v ./pkg/driver/deployment/azure_functions_test.go -run TestDeployFunction | ||
|
||
- name: Run Cleanup Test | ||
run: go test -v ./pkg/driver/deployment/azure_functions_test.go -run TestCleanup |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
# azurefunctionsconfig.yaml | ||
azurefunctionsconfig: | ||
resource_group: ExperimentResourceGroup # Name of the resource group | ||
storage_account_name: testinvitrostorage # Name of the storage account | ||
function_app_name: testinvitrofunctionapp # Name of the function app | ||
location: EastUS # Region where resource created |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"version": "2.0", | ||
"logging": { | ||
"applicationInsights": { | ||
"samplingSettings": { | ||
"isEnabled": true, | ||
"excludedTypes": "Request" | ||
} | ||
} | ||
}, | ||
"extensionBundle": { | ||
"id": "Microsoft.Azure.Functions.ExtensionBundle", | ||
"version": "[4.*, 5.0.0)" | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"IsEncrypted": false, | ||
"Values": { | ||
"FUNCTIONS_WORKER_RUNTIME": "python", | ||
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing", | ||
"AzureWebJobsStorage": "" | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
azure-functions | ||
numpy>=1.21,<1.26 | ||
psutil>=5.9,<6.0 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import time | ||
import socket | ||
import json | ||
import azure.functions as func | ||
import logging | ||
|
||
from .exec_func import execute_function | ||
|
||
# Global variable for hostname | ||
hostname = socket.gethostname() | ||
|
||
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") | ||
|
||
# Directly call the execute_function | ||
duration = execute_function("",runtime_milliseconds,memory_mebibytes) | ||
result_msg = f"Workload completed in {duration} microseconds" | ||
|
||
# Prepare the response | ||
response = { | ||
"Status": "Success", | ||
"Function": req.url.split("/")[-1], | ||
"MachineName": hostname, | ||
"ExecutionTime": int((time.time() - start_time) * 1_000_000), # Total time (includes HTTP, workload, and response prep) | ||
"DurationInMicroSec": duration, # Time spent on the workload itself | ||
"MemoryUsageInKb": memory_mebibytes * 1024, | ||
"Message": result_msg | ||
} | ||
|
||
logging.info(f"Response: {response}") | ||
|
||
return func.HttpResponse( | ||
json.dumps(response), | ||
status_code=200, | ||
mimetype="application/json" | ||
) |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is exactly the same as in the server/trace-func-py. Please leave only one of them. And add the other one in .gitignore file so we won't see it added in |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import math | ||
from time import process_time_ns | ||
from numpy import empty, float32 | ||
from psutil import virtual_memory | ||
|
||
|
||
def execute_function(input, runTime, totalMem): | ||
startTime = process_time_ns() | ||
|
||
chunkSize = 2**10 # size of a kb or 1024 | ||
totalMem = totalMem*(2**10) # convert Mb to kb | ||
memory = virtual_memory() | ||
used = (memory.total - memory.available) // chunkSize # convert to kb | ||
additional = max(1, (totalMem - used)) | ||
array = empty(additional*chunkSize, dtype=float32) # make an uninitialized array of that size, uninitialized to keep it fast | ||
# convert to ns | ||
runTime = (runTime - 1)*(10**6) # -1 because it should be slighly below that runtime | ||
memoryIndex = 0 | ||
while process_time_ns() - startTime < runTime: | ||
for i in range(0, chunkSize): | ||
sin_i = math.sin(i) | ||
cos_i = math.cos(i) | ||
sqrt_i = math.sqrt(i) | ||
array[memoryIndex + i] = sin_i | ||
memoryIndex = (memoryIndex + chunkSize) % additional*chunkSize | ||
return (process_time_ns() - startTime) // 1000 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"bindings": [ | ||
{ | ||
"authLevel": "anonymous", | ||
"type": "httpTrigger", | ||
"direction": "in", | ||
"name": "req", | ||
"methods": ["post"] | ||
}, | ||
{ | ||
"type": "http", | ||
"direction": "out", | ||
"name": "$return" | ||
} | ||
], | ||
"scriptFile": "azurefunctionsworkload.py" | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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": 5, | ||
"WarmupDuration": 0, | ||
|
||
"IsPartiallyPanic": false, | ||
"EnableZipkinTracing": false, | ||
"EnableMetricsScrapping": false, | ||
"MetricScrapingPeriodSeconds": 15, | ||
"AutoscalingMetric": "concurrency", | ||
|
||
"GRPCConnectionTimeoutSeconds": 15, | ||
"GRPCFunctionTimeoutSeconds": 900, | ||
|
||
"DAGMode": false | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -262,4 +262,48 @@ Note: | |||||
- Under `Manage Quota`, select `AWS Lambda` service and click `View quotas` (Alternatively, click [here](https://us-east-1.console.aws.amazon.com/servicequotas/home/services/lambda/quotas)) | ||||||
- Under `Quota name`, select `Concurrent executions` and click `Request increase at account level` (Alternatively, click [here](https://us-east-1.console.aws.amazon.com/servicequotas/home/services/lambda/quotas/L-B99A9384)) | ||||||
- Under `Increase quota value`, input `1000` and click `Request` | ||||||
- Await AWS Support Team to approve the request. The request may take several days or weeks to be approved. | ||||||
- Await AWS Support Team to approve the request. The request may take several days or weeks to be approved. | ||||||
|
||||||
## Using Azure Functions | ||||||
|
||||||
**Pre-requisites:** | ||||||
1. Microsoft Azure account with an active subscription ID | ||||||
2. Existing Service Principal for authentication (refer to Notes section) | ||||||
3. Go installed | ||||||
4. Python3 installed | ||||||
|
||||||
**Quick Setup for Azure Deployment:** | ||||||
1. Install the Azure CLI and verify installation: | ||||||
```bash | ||||||
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash | ||||||
az --version | ||||||
``` | ||||||
2. Use existing Service Principal credentials in order login to Azure. | ||||||
```bash | ||||||
az login --service-principal --username $AZURE_APP_ID --password $AZURE_PASSWORD --tenant $AZURE_TENANT | ||||||
``` | ||||||
> Refer to Note section for generation of Service Principal credentials | ||||||
3. Start the Azure Functions deployment experiment: | ||||||
```bash | ||||||
go run cmd/loader.go --config cmd/config_azure_trace.json | ||||||
``` | ||||||
--- | ||||||
Notes: | ||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Overall, can this be done via Azure web console? This is much simpler than doing it via CLI, since this part you don't need to do in experiment environment. |
||||||
- Service Principal must be created before running experiment, as some environments do not have browsers (e.g. CloudLab). Perform these steps in an environment that allows launching of browser and use the generated credentials. | ||||||
- Log in as a user (Note: This will open a browser window to select Azure account): | ||||||
```bash | ||||||
az login | ||||||
``` | ||||||
- Create an Azure Service Principal: | ||||||
```bash | ||||||
az ad sp create-for-rbac --name "InVitro" --role Contributor --scopes /subscriptions/<your-subscription-id> | ||||||
``` | ||||||
- Set the following values in the environment that the experiment is being run and return to Step 2 of setup: | ||||||
```bash | ||||||
export AZURE_APP_ID=<appId> | ||||||
export AZURE_PASSWORD=<password> | ||||||
export AZURE_TENANT=<tenant> | ||||||
``` | ||||||
leokondrashov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
- Current deployment is via ZIP | ||||||
- Python is used for deployment workload as Go is not supported in Consumption Plan |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of whole this file, you can add the directory into modules to test in
unit-tests.yaml
. Either way, you run all of the tests that you wrote in azure_functions_test.go but you specify all of them one-by-one, which is prone to errors (add test, forgot to test or the opposite, it fails because you removed one).