How to use Juju with Microsoft Azure

Based on the Juju documentation and personal experience.

Juju version used is 3.6-beta2.

Generally there are 2 ways to make Juju work with Azure (for it to be able to create, delete, modify resources on Azure). Each of them has some variations:

1. Old, non-recommended service-principal-credentials way

Juju would copy the user’s credential secrets to the controller and use that when making API calls to the cloud. Credentials are stored. If you require juju version earlier than 3.6-beta - use this method.

2. New, recommended Managed Identity (MI) way

The controller gets the authorisation it needs to manage cloud resources from the Managed Identity. No credentials stored.

Option a) pre-created MI with managed-identity credentials

Option b) pre-created MI with service-principal-secret credentials

Option c) service-principal-secret credentials, let juju create MI

Limitations:

  • Only supported starting juju 3.6 (currently beta)
  • Juju cli should be on Azure VM for it to be able to reach cloud metadata endpoint.
  • Managed Identity and the Juju resources should be on the same Azure subscription

I will try to document all options we have with examples.

Method 1 : Service-principal credentials way

Perform cli app registration in Microsoft Entra ID, to allow juju cli create/modify and delete Azure resources.

Let’s call this app ‘jujucli’ and create a secret for it:

az ad app create --display-name jujucli
appid=$(az ad app list --display-name jujucli --query "[].{id: appId}" -o tsv)
az ad app credential list --id $appid --query keyId -o tsv
az ad app credential reset --id $appid
{
  "appId": "xxxxxx",
  "password": "qqqqq",
  "tenant": "zzzzz"
}

Create service principal:

az ad sp create --id $appid
spObjectId=$(az ad sp show --id $appid --query id -o tsv)

Now let’s grant this application Owner role on a resource we want to use, for example resource group(1) of full subscription(2):

(1) az role assignment create --assignee $spObjectId --role Owner --scope /subscriptions/xxxxxx/resourcegroups/jujuclitest
(2) az role assignment create --assignee $spObjectId --role Owner --scope /subscriptions/xxxxxx

Now inside our resource group (or subscription) Access Control IAM tab we should see this application as owner:

IMAGE 2024-08-16 12:28:04

Now we can use those credentials with:

cat credentials1.yaml
credentials:
  azure:
    azure-service-principal:
      auth-type: service-principal-secret
      application-id: xxxxxx
      application-password: qqqq
      subscription-id: aaaaaa

Now setup the juju region for your controller and future operations so we don’t have to do it with every command (assuming we are working in the same region most of the time anyway):

juju default-region azure eastus

Add the credential:

juju add-credential -f credentials1.yaml --client azure
Credential "azure-service-principal" added locally for cloud "azure".

Bootstrap the controller with:

$ juju bootstrap --constraints allocate-public-ip=false --config resource-group-name=jujuclitest --config network=VNET --credential azure-service-principal azure

$ juju add-model opensearch --config resource-group-name=jujuclitest --config network=VNET

$ juju add-machine --constraints="instance-type=Standard_D4s_v5 allocate-public-ip=false"

Method 2: Managed Identity (MI) way

Prerequisite: This method requires us to have juju CLI inside Azure for it to be able to reach cloud metadata endpoint, so let’s create a small Azure VM for this and install juju there:

az vm create --resource-group MyResourceGroup --name jujucli  --image Ubuntu2204 --location eastus --size Standard_B1s --admin-username ubuntu --ssh-key-value "$(az sshkey show --resource-group TT --name azurekey --query publicKey -o tsv)" --public-ip-sku Standard --zone 1 
ssh ubuntu@VM_IP
sudo snap install juju --channel 3.6/beta

For Options a) and b) We will be required to pre-create Managed Identity first, so let’s start with this part.

Creating Managed Identity

Let’s create 2 identities - subscription scoped and resource group scoped to test both:

export group=jujuclitest
export location=eastus
export role=jujuclitestrole
export role2=jujuclitestrole2
export identityname=jujuclitestidentity
export identityname2=jujuclitestidentity2
export subscription=xxxxx

az group create --name "${group}" --location "${location}"

# One subscription scope identity, role and role assignment:
az identity create --resource-group "${group}" --name "${identityname}"

mid=$(az identity show --resource-group "${group}" --name "${identityname}" --query principalId --output tsv)

az role definition create --role-definition "{
  	\"Name\": \"${role}\",
  	\"Description\": \"Role definition for a Juju controller\",
  	\"Actions\": [
            	\"Microsoft.Compute/*\",
            	\"Microsoft.KeyVault/*\",
            	\"Microsoft.Network/*\",
            	\"Microsoft.Resources/*\",
            	\"Microsoft.Storage/*\",
            	\"Microsoft.ManagedIdentity/userAssignedIdentities/*\"
  	],
  	\"AssignableScopes\": [
        	\"/subscriptions/${subscription}\"
  	]
  }"

az role assignment create --assignee-object-id "${mid}" --assignee-principal-type "ServicePrincipal" --role "${role}" --scope "/subscriptions/${subscription}"

# Second resource group scope identity, role and role assignment:
az identity create --resource-group "${group}" --name "${identityname2}"

mid=$(az identity show --resource-group "${group}" --name "${identityname2}" --query principalId --output tsv)

az role definition create --role-definition "{
  	\"Name\": \"${role2}\",
  	\"Description\": \"Role definition for a Juju controller\",
  	\"Actions\": [
            	\"Microsoft.Compute/*\",
            	\"Microsoft.KeyVault/*\",
            	\"Microsoft.Network/*\",
            	\"Microsoft.Resources/*\",
            	\"Microsoft.Storage/*\",
            	\"Microsoft.ManagedIdentity/userAssignedIdentities/*\"
  	],
  	\"AssignableScopes\": [
        	\"/subscriptions/${subscription}/resourcegroups/${group}\"
  	]
  }"

az role assignment create --assignee-object-id "${mid}" --assignee-principal-type "ServicePrincipal" --role "${role2}" --scope "/subscriptions/${subscription}/resourcegroups/${group}"

If we look at our Access control - Role Assignment tab inside our resource group ‘jujucli’ this is what we should see now:

image

One last important thing now - the MIs are created now, but we should grant our VM with juju the permission to use the MIs. Without this you will be getting and error (ManagedIdentityCredential authentication failed) trying to do the controller bootstrap and other operations.

For MI1:

az vm identity assign --resource-group MyResourceGroup --name jujucli --identities /subscriptions/${subscription}/resourceGroups/${group}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/${identityname}

For MI2:

az vm identity assign --resource-group MyResourceGroup --name jujucli --identities /subscriptions/${subscription}/resourceGroups/${group}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/${identityname2}

Now if we look at our VM - Security - Identity - User assigned tab we should see both MI’s there:

image

Now let’s try to use our MIs to actually bootstrap a controller and deploy something.

Option a) - pre-created MI with managed-identity credentials

$ cat credentials.yaml
credentials:
  azure:
    azure-option-one:
      auth-type: managed-identity
      managed-identity-path: jujuclitest/jujuclitestidentity
      subscription-id: xxxx

Add the credential:

juju add-credential -f credentials.yaml --client azure
Credential "azure-option-one" added locally for cloud "azure".

Now setup the juju region for your controller and future operations so we don’t have to do it with every command (assuming we are working in the same region most of the time anyway):

juju default-region azure eastus

Try to bootstrap the controller:

$ juju bootstrap azure mycontroller
...
Bootstrap complete, controller "mycontroller" is now available
Controller machines are in the "controller" model

As your credentials are subscription scope, you can operate in any resource group, the controller VM will be created of type Standard_DS1_v2 in random resource group like ‘juju-controller-xxxx’ and placed in ‘juju-internal-network/juju-controller-subnet’.

You can also specify the resource group and network for controller like this:

juju bootstrap azure --config resource-group-name=jujuclitest --config network=jujuclitest mycontroller 

Now if you :

juju add-model test
juju add-machine

As your credentials are subscription scope, you can operate in any resource group, the VM of type Standard A1 v2 will be created in random resource group like ‘juju-test-xxxx’ and placed in ‘‘juju-internal-network/juju-internal-subnet’

If you would like to specify those, you will have to do:

juju add-model test --config resource-group-name=jujuclitest --config network=jujuclitest 

For adding machines, I usually use the following way:

juju add-machine --constraints="instance-type=Standard_B1s allocate-public-ip=false"

Now let’s try resource group scope credential:

$ cat credentials2.yaml
credentials:
  azure:
    azure-option-two:
      auth-type: managed-identity
      managed-identity-path jujuclitest/jujuclitestidentity2
      subscription-id: xxxx

Add the credential:

juju add-credential -f credentials2.yaml --client azure
Credential "azure-option-two" added locally for cloud "azure".

Try to bootstrap the controller:

$ juju bootstrap azure --credential azure-option-two mycontroller

you will see the following error message: { “error”: { “code”: “AuthorizationFailed”, “message”: “The client with object id ‘xxx’ does not have authorization to perform action ‘Microsoft.Authorization/roleAssignments/read’ over scope ‘/subscriptions/xxx/resourceGroups/juju-controller-4f1c32d9/providers/Microsoft.Authorization’ or the scope is invalid. If access was recently granted, please refresh your credentials.” } }

this is because our MI is only for the particular resource group, so we can only operate there, let’s specify the group explicitly:

$ juju bootstrap azure --config resource-group-name=jujuclitest --credential azure-option-two mycontroller

The rest of the model/VM operations are similar, except that resource group and network in that resource group could be used only:

juju add-model test --config resource-group-name=jujuclitest --config network=jujuclitest 

juju add-machine --constraints="instance-type=Standard_B1s allocate-public-ip=false"

Option b) pre-created MI with service-principal-secret credentials

This method is similar - we will be using same MI’s but different type of credentials. But first let’s create service-principal secret.

Follow the Method 1 : Service-principal credentials way to create credentials, but don’t bootstrap the controller using Method 1. Once you have the credentials using Method1, bootstrap the controller and Juju will create another MI for us:

juju bootstrap azure --constraints "instance-role=X”

Instance role could be:

a) subscription/resourcegroup/identityname (MI in different subscription and resourcegroup)

b) resourcegroup/identityname (MI in different resourcegroup)

c) identityname (MI in same resource group)

Let’s use option C here as our MI is in the same resource group:

juju bootstrap azure --config resource-group-name=jujuclitest --constraints "instance-role=jujuclitestidentity" --credential azure-option-three mycontroller
…
Bootstrap complete, controller "mycontroller" is now available
Controller machines are in the "controller" model

The issue with this method however is that when you run Juju kill-controller mycontroller, the controller VM stays alive on Azure.

Option c) service-principal-secret credentials, let juju create MI

To use this method juju will try to create a deployment with MI, and if we use only resource group scoped credentials as in previous method bootstrap will fail.

Follow the Method 1 : Service-principal credentials way to create credentials, but don’t bootstrap the controller using Method 1.

We will need to wider the scope for our credentials first, let’s give it full subscription scope:

az role assignment create --assignee $spObjectId --role Owner --scope /subscriptions/xxxxxx

Now we can bootstrap the controller:

juju bootstrap azure --config resource-group-name=jujuclitest --constraints "instance-role=auto" --credential azure-option-three mycontroller
Bootstrap complete, controller "mycontroller" is now available
Controller machines are in the "controller" model

We will see an MI created in our resource group: image

The issue with this method however is that when you run Juju kill-controller mycontroller, the controller VM stays alive on Azure.

Hope you enjoyed this tutorial and successfully deployed your project with Juju on Azure.

2 Likes

Hi! Thank you for this write-up! In an attempt to understand what we’re missing from the published docs, I’ve discussed this with @wallyworld . Some notes:

  • Regarding the Limitations: The second and the third actually only apply to option (a).
  • Regarding Method 1: None of the steps related to the service-principal-secret should be necessary – all of them, including setting up the Juju application, are steps Juju does automatically for you when you add the credential in the interactive mode. It sounds like the problem here was that your account somehow didn’t have access to our precreated Juju application. @wallyworld would be happy to work with you to investigate further.
  • Regarding everything else: The remaining details seem right and are as documented in the published doc (https://juju.is/docs/juju/microsoft-azure). (Though you’re right that it can be confusing to have the same info on New 3.6 feature: support for Azure managed identities as well, especially when given in a slightly different form – we’ll update that forum post to merely point to the published doc, and then try to polish the form in the published doc further so it’s easier to follow along.)

None of the steps related to the service-principal-secret should be necessary – all of them, including setting up the Juju application, are steps Juju does automatically for you when you add the credential in the interactive mode.

I have a slightly different take on the matter to be honest.

In my opinion, if we say in our doc there are three methods:

  • authentication types: [interactive, service-principal-secret, managed-identity]

Then we should be documenting all 3 types, how to use them.

By saying “forget about service-principal-secret just use interactive” we are essentially saying to the customer that method 2 is either not working or not supported, which is not the case, as it’s just not documented.

In my opinion, if we say in our doc there are three methods:

  • authentication types: [interactive, service-principal-secret, managed-identity]

Then we should be documenting all 3 types, how to use them.

The interactive type is not really a credential type – we should fix that in the CLI and docs. So, there are in fact just two credential types: service-principal-secret and managed-identity.

You can use either one in the usual manner for a machine cloud – with the caveat that

  • the service-principal-secret seems to have a bug (your account somehow not having access to the precreated Juju application) and
  • the managed-identity type has some requirements (you must operate from the Azure Cloud shell or a jump host running in Azure) and limitations (only available in beta, the managed identity and the Juju resources must be created on the same subscription)
  • for both we really only support the interactive mode (cf. Juju team consensus yesterday).

Where the story gets a little confusing is that you can combine the authentication types the two “credential” types represent by using credential type service-principal-secret (in the add-credential step) with bootstrap constraint instance-role set to the managed identity (in the boostrap step), the effect being that the client authenticates with the cloud (for the purpose of bootstrap) using the service-principal-secret but the controller authenticates with the cloud (for all subsequent provisioning) using the managed identity.

By saying “forget about service-principal-secret just use interactive” we are essentially saying to the customer that method 2 is either not working or not supported, which is not the case, as it’s just not documented.

We are not saying that. We’re just saying: All machine cloud credentials can in principle be provided to Juju in 3 ways: Interactively, via a YAML file, or automatically, by having Juju read the available credential information on your system (from YAML files, environment variables, or rc files) (see Juju | How to manage credentials – 2a-b-c). However, we do not recommend using the YAML method (cf. Juju team consensus yesterday), so for Azure there’s effectively just one method overall – the interactive method. So, please add this credential type service-principal-secret using the interactive method.

I’ll see how we can make this clearer in the docs (e.g., the Azure doc should maybe give the 3 methods in the credential section, not the bootstrap section). From what I can see, though, the biggest issue at this point is to ensure that all Azure accounts, including yours, can find the Juju application so that the service-principal-secret works, and @wallyworld can help you with that.

The interactive type is not really a credential type – we should fix that in the CLI and docs. So, there are in fact just two credential types: service-principal-secret and managed-identity .

Correction: The interactive type is however an authentication type. Updated the docs again to more accurately capture this and the related workflows (especially in light of a very recent PR). PS The word interactive here is misleading as in the Juju CLI it usually refers to commands that have an interactive mode, and here that’s true as well but it’s not what the word is referring to – here it denotes something more along the lines of service-principal-secret-via-browser. The Juju team will change it to that.

@alitvinov thank you for these guides, they have been very very helpful, to set up juju on Azure.

The juju docs page still looks a bit rough, and somewhat misleading as it nudges to the interactive way that still has some hiccups, e.g. Bug #2077173 “Azure interactive credentials not working” : Bugs : Canonical Juju, and does not work on stable versions, like 3.5

1 Like