From e32aaa339cb75dbb87645385ade9f7971d7e2ebb Mon Sep 17 00:00:00 2001 From: Zalan Blenessy Date: Fri, 8 Mar 2024 15:22:09 +0100 Subject: [PATCH] Azure: add support for User-assigned Managed Identities Facilitates means for passwordless use-cases in Azure. Similar to iam-instance-profile in AWS. Change-Id: I0de170161cef78acd8016a0f98cd5a91c1d4999e --- doc/source/azure.rst | 29 ++++++++++++++++++++++++ nodepool/driver/azure/adapter.py | 18 +++++++++++++++ nodepool/driver/azure/azul.py | 5 ++++ nodepool/driver/azure/config.py | 8 +++++++ nodepool/tests/fixtures/azure.yaml | 4 ++++ nodepool/tests/unit/test_driver_azure.py | 16 +++++++++++++ 6 files changed, 80 insertions(+) diff --git a/doc/source/azure.rst b/doc/source/azure.rst index d80e69f86..fd43d73ce 100644 --- a/doc/source/azure.rst +++ b/doc/source/azure.rst @@ -749,6 +749,33 @@ section of the configuration. If given, the size of the operating system disk, in GiB. + .. attr:: user-assigned-identities + :type: dict + :default: None + + `User-assigned Managed Identities`_ to assign to the VM. + Useful for giving access to services without needing any secrets. + + .. attr:: name + :required: + :type: str + + The name of the User-assigned Managed Identity. + + .. attr:: resource-group + :type: str + :default: The provider's resource group + + Overrides :attr:`providers.[azure].resource-group`. + + For example: + + .. code-block:: yaml + + user-assigned-identities: + - name: myLocalIdentity + - name: myRemoteIdentity + resource-group: remote-rg .. _`Azure CLI`: https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli?view=azure-cli-latest @@ -757,3 +784,5 @@ section of the configuration. .. _`Azure User Data`: https://docs.microsoft.com/en-us/azure/virtual-machines/user-data .. _`Azure Custom Data`: https://docs.microsoft.com/en-us/azure/virtual-machines/custom-data + +.. _`User-assigned Managed Identities`: https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/qs-configure-rest-vm diff --git a/nodepool/driver/azure/adapter.py b/nodepool/driver/azure/adapter.py index a773cfb0f..428bdff3b 100644 --- a/nodepool/driver/azure/adapter.py +++ b/nodepool/driver/azure/adapter.py @@ -26,6 +26,7 @@ from nodepool.driver.utils import ( RateLimiter, ImageUploader, ) +from urllib.parse import urlparse from nodepool.driver import statemachine from nodepool import exceptions from . import azul @@ -677,6 +678,23 @@ class AzureAdapter(statemachine.Adapter): if label.user_data: spec['properties']['userData'] = label.user_data + # build resource id for all configured User-assigned Identities + uai_resource_ids = set() + for uai in label.user_assigned_identities: + uai_rg_name = uai.get("resource-group", self.resource_group) + uai_url = self.azul.managed_identities.url( + resourceGroupName=uai_rg_name, resourceName=uai['name']) + uai_resource_ids.add(urlparse(uai_url).path) + + # adding empty userAssignedIdentities is not allowed by Azure + if uai_resource_ids: + spec['identity'] = { + 'type': 'UserAssigned', + 'userAssignedIdentities': { + rid: {} for rid in uai_resource_ids + }, + } + with self.rate_limiter: self.log.debug(f"Creating VM {hostname}") return self.azul.virtual_machines.create( diff --git a/nodepool/driver/azure/azul.py b/nodepool/driver/azure/azul.py index 9b0f92d7c..a1d86c165 100644 --- a/nodepool/driver/azure/azul.py +++ b/nodepool/driver/azure/azul.py @@ -268,6 +268,11 @@ class AzureCloud: providerId='Microsoft.Compute', resource='skus', apiVersion='2019-04-01') + self.managed_identities = AzureResourceProviderCRUD( + self, + providerId='Microsoft.ManagedIdentity', + resource='userAssignedIdentities', + apiVersion='2023-01-31') def get(self, url, codes=[200]): return self.request('GET', url, None, codes) diff --git a/nodepool/driver/azure/config.py b/nodepool/driver/azure/config.py index 7a8761239..d15c07ea3 100644 --- a/nodepool/driver/azure/config.py +++ b/nodepool/driver/azure/config.py @@ -193,6 +193,8 @@ class AzureLabel(ConfigValue): self.custom_data = self._encodeData(label.get('custom-data', None)) self.host_key_checking = self.pool.host_key_checking self.volume_size = label.get('volume-size') + self.user_assigned_identities = label.get( + 'user-assigned-identities', []) def _encodeData(self, s): if not s: @@ -205,6 +207,11 @@ class AzureLabel(ConfigValue): v.Required('vm-size'): str, } + user_assigned_identities = { + v.Required('name'): str, + 'resource-group': str, + } + return { v.Required('name'): str, 'cloud-image': str, @@ -215,6 +222,7 @@ class AzureLabel(ConfigValue): 'user-data': str, 'custom-data': str, 'volume-size': int, + 'user-assigned-identities': [user_assigned_identities], } diff --git a/nodepool/tests/fixtures/azure.yaml b/nodepool/tests/fixtures/azure.yaml index 7aa6516cb..5fbc9c51b 100644 --- a/nodepool/tests/fixtures/azure.yaml +++ b/nodepool/tests/fixtures/azure.yaml @@ -93,6 +93,10 @@ providers: dynamic-tenant: "Tenant is {{request.tenant_name}}" user-data: "This is the user data" custom-data: "This is the custom data" + user-assigned-identities: + - name: localid + - name: otherid + resource-group: othergroup - name: image-by-name cloud-image: image-by-name hardware-profile: diff --git a/nodepool/tests/unit/test_driver_azure.py b/nodepool/tests/unit/test_driver_azure.py index 9480a9dc6..3efba2709 100644 --- a/nodepool/tests/unit/test_driver_azure.py +++ b/nodepool/tests/unit/test_driver_azure.py @@ -99,6 +99,22 @@ class TestDriverAzure(tests.DBTestCase): self.fake_azure.crud['Microsoft.Compute/virtualMachines']. requests[0]['properties']['userData'], 'VGhpcyBpcyB0aGUgdXNlciBkYXRh') # This is the user data + self.assertEqual( + self.fake_azure.crud['Microsoft.Compute/virtualMachines']. + requests[0]['identity'], + { + "type": "UserAssigned", + "userAssignedIdentities": { + f"/subscriptions/{self.fake_azure.subscription_id}/" + "resourceGroups/nodepool/" + "providers/Microsoft.ManagedIdentity/" + "userAssignedIdentities/localid": {}, + f"/subscriptions/{self.fake_azure.subscription_id}/" + "resourceGroups/othergroup/" + "providers/Microsoft.ManagedIdentity/" + "userAssignedIdentities/otherid": {}, + } + }) tags = (self.fake_azure.crud['Microsoft.Compute/virtualMachines']. requests[0]['tags']) self.assertEqual(tags.get('team'), 'DevOps')