AB#2413: Add Azure function for CVMs

Add code of an azure function that is a
close copy of the existing cloud function on google.
The function spawns a CVM and initializes it
as a GitHub runner. The tag is 'azure-cvm'.
This commit is contained in:
Otto Bittner 2022-09-20 10:55:54 +02:00
parent de9bdaef24
commit 13f973f61e
9 changed files with 609 additions and 0 deletions

22
.github/runners/azure-cvm/README.md vendored Normal file
View File

@ -0,0 +1,22 @@
# General
This folder contains the files to setup an Azure function and ARM template in order to deploy Azure CVMs with a webhook.
- `cvm-template.json`: An ARM template that deploys one CVM and the required resources. It is deployed by the Azure Function
- `azure-function`: All necessary files to redeploy the function. Changes in `requirements.txt` are installed during deployment of the function. `cloud-init.txt` is put into the CVM by supplying it as a parameter to the ARM template deployment.
# Update cvm-template
- Look for the `Template spec` resource in your Azure project (e.g. "snp-value-reporter-template").
- Click on "Create new version".
- Select the latest version available.
- Use `current_version+1` as new version.
- Go to "Edit template" and make your changes.
- Go to "Review + Save" and save your changes.
# Deploy azure function
Background info can be found in the [Azure docs](https://learn.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-python?tabs=azure-cli%2Cbash%2Cbrowser#deploy-the-function-project-to-azure).
To deploy your Azure CLI needs to be authenticated and [Azure Function Core Tools](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=v4%2Clinux%2Ccsharp%2Cportal%2Cbash#v2) needs to be installed.
```bash
cd .github/runners/azure-cvm/azure-function
func azure functionapp publish edgeless-snp-reporter
```

View File

@ -0,0 +1,4 @@
.git*
.vscode
local.settings.json
test

View File

@ -0,0 +1,135 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that dont work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Azure Functions artifacts
bin
obj
appsettings.json
local.settings.json
# Azurite artifacts
__blobstorage__
__queuestorage__
__azurite_db*__.json
.python_packages

View File

@ -0,0 +1,132 @@
import os
import logging
import hmac
import hashlib
import random
import string
import base64
import azure.functions as func
from azure.mgmt.resource import ResourceManagementClient
from azure.mgmt.resource.resources.v2021_04_01.models import Deployment, DeploymentProperties
from azure.keyvault.secrets import SecretClient
from azure.identity import DefaultAzureCredential
LABEL = "azure-cvm"
SUBSCRIPTION_ID = "0d202bbb-4fa7-4af8-8125-58c269a05435"
VAULT_URL = "https://github-token.vault.azure.net/"
TOKEN_SECRET_NAME = "gh-webhook-secret"
SSH_KEY_SECRET_NAME = "snp-reporter-pubkey"
def main(req: func.HttpRequest) -> func.HttpResponse:
logging.info('Python HTTP trigger function processed a request.')
allow, reason = authorize(req)
if not allow:
return func.HttpResponse(f'unauthorized: {reason}', status_code=404)
request_json = req.get_json()
if request_json and 'action' in request_json:
if request_json['action'] == 'queued':
return job_queued(request_json['workflow_job'])
elif request_json['action'] == 'completed':
return job_completed(request_json['workflow_job'])
elif request_json['action'] == 'in_progress':
return f'nothing to do here'
else:
return func.HttpResponse(f'invalid message format', status_code=400)
def authorize(request) -> (bool, str) :
credentials = DefaultAzureCredential()
client = SecretClient(vault_url=VAULT_URL, credential=credentials)
correct_token = client.get_secret(TOKEN_SECRET_NAME).value
if correct_token is None:
return False, 'correct token not set'
correct_hmac = 'sha256=' + hmac.new(correct_token.encode('utf-8'), request.get_body(), hashlib.sha256).hexdigest()
request_hmac = request.headers.get('X-Hub-Signature-256')
if request_hmac is None:
return False, 'X-Hub-Signature-256 not set'
if correct_hmac == request_hmac:
return True, ''
else:
return False, f'X-Hub-Signature-256 incorrect'
def job_queued(workflow_job) -> str:
if not LABEL in workflow_job['labels']:
return func.HttpResponse(f'irrelevant job labels: {workflow_job["labels"]}', status_code=200)
cloud_init = generate_cloud_init()
instance_uid = ''.join(random.choice(string.ascii_lowercase + string.digits) for i in range(6))
credentials = DefaultAzureCredential()
client = SecretClient(vault_url=VAULT_URL, credential=credentials)
ssh_key = client.get_secret(SSH_KEY_SECRET_NAME).value
try:
create_cvm(instance_uid, cloud_init, ssh_key)
except Exception as e:
return func.HttpResponse(f'creating instance failed: {e}', status_code=400)
return 'success'
def job_completed(workflow_job) -> str:
if not LABEL in workflow_job['labels']:
return func.HttpResponse(f'irrelevant job labels: {workflow_job["labels"]}', status_code=200)
instance_name = workflow_job["runner_name"]
try:
delete_cvm(machine_name=instance_name)
except Exception as e:
return func.HttpResponse(f'deleting instance failed: {e}', status_code=400)
return 'success'
def generate_cloud_init() -> str:
path = os.path.join(os.path.dirname(__file__), "cloud-init.txt")
with open(path, "r") as f:
cloud_init = f.read()
return base64.b64encode(cloud_init.encode('utf-8'))
def delete_cvm(machine_name):
credentials = DefaultAzureCredential()
resource_client = ResourceManagementClient(
credentials,
SUBSCRIPTION_ID,
)
path = f"/subscriptions/{SUBSCRIPTION_ID}/resourceGroups/snp-value-reporting/providers"
async_vm_delete = resource_client.resources.begin_delete_by_id(resource_id=f"{path}/Microsoft.Compute/virtualMachines/{machine_name}", api_version="2022-08-01")
async_vm_delete.wait()
async_osdisk_delete = resource_client.resources.begin_delete_by_id(resource_id=f"{path}/Microsoft.Compute/disks/{machine_name}-osdisk", api_version="2022-07-02")
async_nic_delete = resource_client.resources.begin_delete_by_id(resource_id=f"{path}/Microsoft.Network/networkInterfaces/{machine_name}-nic", api_version="2022-08-01")
async_nsg_delete = resource_client.resources.begin_delete_by_id(resource_id=f"{path}/Microsoft.Network/networkSecurityGroups/{machine_name}-nsg", api_version="2022-05-01")
async_vnet_delete = resource_client.resources.begin_delete_by_id(resource_id=f"{path}/Microsoft.Network/virtualNetworks/{machine_name}-vnet", api_version="2022-05-01")
async_ip_delete = resource_client.resources.begin_delete_by_id(resource_id=f"{path}/Microsoft.Network/publicIPAddresses/{machine_name}-ip", api_version="2022-05-01")
async_vnet_delete.wait()
async_nic_delete.wait()
async_ip_delete.wait()
async_nsg_delete.wait()
async_osdisk_delete.wait()
return True
def create_cvm(instance_uid, cloud_init, ssh_key) -> str:
credentials = DefaultAzureCredential()
resource_client = ResourceManagementClient(
credentials,
SUBSCRIPTION_ID,
)
template_id = "https://raw.githubusercontent.com/edgelesssys/constellation/main/.github/runners/azure-cvm/cvm-template.json"
depl_properties = DeploymentProperties(mode="Incremental", template_link={"uri": template_id}, parameters={"instanceUid": {"value": instance_uid}, "customData": {"value": cloud_init.decode("utf-8")}, "pubKey": {"value": ssh_key}})
depl = Deployment(properties=depl_properties)
async_vm_start = resource_client.deployments.begin_create_or_update(
"snp-value-reporting", "snp-value-reporter-deployment", depl)
async_vm_start.wait()
return True

View File

@ -0,0 +1,36 @@
#cloud-config
users:
- default
- name: github-actions-runner-user
groups: docker
sudo: ALL=(ALL) NOPASSWD:ALL
homedir: /home/github-actions-runner-user
package_update: true
packages:
- git
- cryptsetup
- build-essential
- libguestfs-tools
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
- jq
- pv
runcmd:
- [/bin/bash, -c, "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg"]
- [/bin/bash, -c, "echo \"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\" | tee /etc/apt/sources.list.d/docker.list > /dev/null "]
- [apt-get, update]
- [apt-get, install, -y, docker-ce, docker-ce-cli, containerd.io, libssl-dev, pigz, azure-cli]
- [/bin/bash, -c, "sudo service docker start"]
- [mkdir, -p, /actions-runner]
- [curl, -o, "/actions-runner/actions-runner-linux-x64-2.286.1.tar.gz", -L, "https://github.com/actions/runner/releases/download/v2.286.1/actions-runner-linux-x64-2.286.1.tar.gz"]
- [/bin/bash, -c, "cd /actions-runner && tar xzf /actions-runner/actions-runner-linux-x64-2.286.1.tar.gz"]
- [chown, -R, github-actions-runner-user:github-actions-runner-user, /actions-runner]
- [sudo, -u, github-actions-runner-user, /bin/bash, -c, "cd /actions-runner && /actions-runner/config.sh --url https://github.com/edgelesssys/constellation --ephemeral --labels azure-cvm --replace --unattended --token $(curl -X POST -H \"Accept: application/vnd.github+json\" -H \"Authorization: Bearer $(curl -s -H Metadata:true -H \"Authorization: Bearer $(curl -s -H Metadata:true --noproxy \"*\" \"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net\" | jq -r .access_token)\" --noproxy \"*\" \"https://github-token.vault.azure.net/secrets/github-access-token?api-version=2016-10-01\" | jq -r .value)\" https://api.github.com/repos/edgelesssys/constellation/actions/runners/registration-token | jq -r .token)"]
- [/bin/bash, -c, "cd /actions-runner && ./svc.sh install"]
- [/bin/bash, -c, "systemctl enable --now actions.runner.edgelesssys-constellation.$(hostname | cut -c -31).service"]

View File

@ -0,0 +1,20 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}

View File

@ -0,0 +1,15 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[3.*, 4.0.0)"
}
}

View File

@ -0,0 +1,9 @@
# DO NOT include azure-functions-worker in this file
# The Python Worker is managed by Azure Functions platform
# Manually managing azure-functions-worker may cause unexpected issues
azure-functions
azure-mgmt-resource==21.1.0
azure-identity==1.10.0
azure-mgmt-subscription==3.1.1
azure-keyvault==4.2.0

View File

@ -0,0 +1,236 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"instanceUid": {
"type": "string"
},
"customData": {
"type": "string"
},
"pubKey": {
"type": "string"
}
},
"variables": {
"virtualMachineName": "[concat('snp-value-reporter-', parameters('instanceUid'))]",
"osDiskName": "[concat(variables('virtualMachineName'), '-osdisk')]",
"vnetName": "[concat(variables('virtualMachineName'), '-vnet')]",
"nicName": "[concat(variables('virtualMachineName'), '-nic')]",
"nsgName": "[concat(variables('virtualMachineName'), '-nsg')]",
"subnetName": "[concat(variables('vnetName'), '/subnets/default')]",
"nicID": "[concat('/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/snp-value-reporting/providers/Microsoft.Network/networkInterfaces/', variables('nicName'))]",
"osDiskId": "[concat('/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/snp-value-reporting/providers/Microsoft.Compute/disks/', variables('osDiskName'))]",
"nsgId": "[concat('/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/snp-value-reporting/providers/Microsoft.Network/networkSecurityGroups/', variables('nsgName'))]",
"subnetId": "[concat('/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/snp-value-reporting/providers/Microsoft.Network/virtualNetworks/', variables('subnetName'))]",
"imageId": "/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/Providers/Microsoft.Compute/Locations/northeurope/Publishers/canonical/ArtifactTypes/VMImage/Offers/0001-com-ubuntu-confidential-vm-focal/Skus/20_04-lts-cvm/Versions/20.04.202208240"
},
"resources": [
{
"type": "Microsoft.Network/virtualNetworks",
"apiVersion": "2020-11-01",
"name": "[variables('vnetName')]",
"location": "northeurope",
"properties": {
"addressSpace": {
"addressPrefixes": [
"172.20.0.0/16"
]
},
"subnets": [
{
"name": "default",
"properties": {
"addressPrefix": "172.20.0.0/24",
"delegations": [],
"privateEndpointNetworkPolicies": "Disabled",
"privateLinkServiceNetworkPolicies": "Enabled"
}
}
],
"virtualNetworkPeerings": [],
"enableDdosProtection": false
}
},
{
"type": "Microsoft.Network/virtualNetworks/subnets",
"apiVersion": "2020-11-01",
"name": "[concat(variables('vnetName'), '/default')]",
"dependsOn": [
"[resourceId('Microsoft.Network/virtualNetworks', variables('vnetName'))]"
],
"properties": {
"addressPrefix": "172.20.0.0/24",
"delegations": [],
"privateEndpointNetworkPolicies": "Disabled",
"privateLinkServiceNetworkPolicies": "Enabled"
}
},
{
"type": "Microsoft.Network/networkSecurityGroups",
"apiVersion": "2020-11-01",
"name": "[variables('nsgName')]",
"location": "northeurope",
"properties": {
"securityRules": [
{
"name": "SSH",
"properties": {
"protocol": "TCP",
"sourcePortRange": "*",
"destinationPortRange": "22",
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "*",
"access": "Allow",
"priority": 300,
"direction": "Inbound",
"sourcePortRanges": [],
"destinationPortRanges": [],
"sourceAddressPrefixes": [],
"destinationAddressPrefixes": []
}
}
]
}
},
{
"type": "Microsoft.Network/networkSecurityGroups/securityRules",
"apiVersion": "2020-11-01",
"name": "[concat(variables('nsgName'), '/SSH')]",
"dependsOn": [
"[variables('nsgId')]"
],
"properties": {
"protocol": "TCP",
"sourcePortRange": "*",
"destinationPortRange": "22",
"sourceAddressPrefix": "*",
"destinationAddressPrefix": "*",
"access": "Allow",
"priority": 300,
"direction": "Inbound",
"sourcePortRanges": [],
"destinationPortRanges": [],
"sourceAddressPrefixes": [],
"destinationAddressPrefixes": []
}
},
{
"type": "Microsoft.Network/networkInterfaces",
"apiVersion": "2020-11-01",
"name": "[variables('nicName')]",
"dependsOn": [
"[variables('subnetId')]"
],
"location": "northeurope",
"properties": {
"ipConfigurations": [
{
"name": "ipconfig1",
"properties": {
"privateIPAddress": "172.20.0.4",
"privateIPAllocationMethod": "Dynamic",
"subnet": {
"id": "[variables('subnetID')]"
},
"primary": true,
"privateIPAddressVersion": "IPv4"
}
}
],
"dnsSettings": {
"dnsServers": []
},
"enableAcceleratedNetworking": false,
"enableIPForwarding": false,
"networkSecurityGroup": {
"id": "[variables('nsgId')]"
}
}
},
{
"type": "Microsoft.Compute/virtualMachines",
"apiVersion": "2022-03-01",
"name": "[variables('virtualMachineName')]",
"dependsOn": [
"[variables('nicID')]"
],
"identity": {
"type": "UserAssigned",
"userAssignedIdentities": {
"/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/snp-value-reporting/providers/Microsoft.ManagedIdentity/userAssignedIdentities/TokenAccess": {}
}
},
"location": "northeurope",
"zones": [
"3"
],
"properties": {
"hardwareProfile": {
"vmSize": "Standard_DC2as_v5"
},
"storageProfile": {
"imageReference": {
"publisher": "canonical",
"offer": "0001-com-ubuntu-confidential-vm-focal",
"sku": "20_04-lts-cvm",
"version": "latest"
},
"osDisk": {
"osType": "Linux",
"name": "[variables('osDiskName')]",
"createOption": "FromImage",
"caching": "ReadWrite",
"managedDisk": {
"securityProfile": {
"securityEncryptionType": "VMGuestStateOnly"
},
"storageAccountType": "Premium_LRS"
},
"deleteOption": "Delete"
},
"dataDisks": []
},
"osProfile": {
"computerName": "[variables('virtualMachineName')]",
"adminUsername": "azureuser",
"linuxConfiguration": {
"disablePasswordAuthentication": true,
"provisionVMAgent": true,
"patchSettings": {
"patchMode": "ImageDefault",
"assessmentMode": "ImageDefault"
},
"ssh": {
"publicKeys": [
{
"path": "/home/azureuser/.ssh/authorized_keys",
"keyData": "[parameters('pubKey')]"
}
]
}
},
"allowExtensionOperations": true,
"customData": "[parameters('customData')]"
},
"networkProfile": {
"networkInterfaces": [
{
"id": "[variables('nicID')]",
"properties": {
"deleteOption": "Delete"
}
}
]
},
"securityProfile": {
"uefiSettings": {
"secureBootEnabled": true,
"vTpmEnabled": true
},
"securityType": "ConfidentialVM"
}
}
}
]
}