Cloning a VM in Azure from Node.js
Tags: azure, cloud, javascript, node
Published: 2022-03-30 09:00:07

*Photo by <a href="https://unsplash.com/@phillshaw?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Phil Shaw</a> on <a href="https://unsplash.com/s/photos/clone?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>*

Sometimes you need to duplicate a certain virtual machine (VM). This may be necessary to get not only the same base data (operating system, installed programs, user accounts and settings), but also the same VM configuration such as the used number of cores, memory, and network settings.

One area where this might be helpful is if you want to create a test setup, where each test should start at the same kind of VM. Another interesting area is to provide a standardized VM to each employee or customer. The approach of cloning helps in that regard, as a "template" (or clone master, i.e., the source VM for cloning) can be updated and taken care of - having direct impact on the cloning without needing to update the code.

## Basic Setup

In order to deal with it efficiently, I've decided to create a small Node.js powered service that does all the orchestration for me. The most important packages to utilize have been:

- `@azure/arm-compute` (brings the actual VM orchestration)
- `@azure/arm-network` (ability to control the network plane, i.e., create a new virtual ethernet adapter)
- `@azure/identity` (for the actual authorization of other Azure management packages)

Our orchestration needs require elevated rights. This can be done with an Azure service principal.

What you'll need:

- Your `tenant`
- The `subscription` ID
- The `clientId` of a created service principal
- The associated `clientSecret` of the created service principal
- The `resourceGroup` where the reference VM (and the cloned VM) will reside
- The name of the reference VM (`referenceVmName`) to use as a template for cloning

The service principal can be created via the Azure CLI as explained on [the documentation](https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli).

What we need now is the following imports and creation of the credentials:

```js
const { ClientSecretCredential } = require("@azure/identity");
const { ComputeManagementClient } = require("@azure/arm-compute");
const { NetworkManagementClient } = require("@azure/arm-network");

const credential = new ClientSecretCredential(tenant, clientId, clientSecret);
```

At some later point in time we can create the actual management clients and do something useful with them. As an example, we could just list all the available VMs in the provided resource group or try to locate the reference VM in them. If the reference VM is not there we might want to error out.

```js
const computeClient = new ComputeManagementClient(credential, subscription);
const networkClient = new NetworkManagementClient(credential, subscription);
const machines = await computeClient.virtualMachines.list(resourceGroup);
const referenceVm = machines.find((m) => m.name === referenceVmName);
```

With those prerequisites in mind we can have a look at the actual cloning process.

## Cloning Process

Cloning is the process of making an exact copy. Unfortunately, the copy cannot be 100% exact. For instance, since its a different machine we require a dedicated network adapter that also exposes, for instance, a different IP address. Also, some system internals such as the processor ID will definitely be different.

Before we can actually clone the VM we need to create the other (required) resources:

- A network adapter
- An (OS) disk

While cloning of the disk works by taking the template and copying it, the other resources are just created via the API. We will still copy some properties from the template VM, however, many interesting parts (e.g., the public IP allocation method of the network adapter) are directly specified.

Without further ado, here's the code for creating the network adapter.

```js
async function createNetwork(networkClient, vm, prefix) {
  const [nic] = vm.networkProfile.networkInterfaces;
  const networks = await networkClient.networkInterfaces.list(resourceGroup);
  const network = networks.find((m) => m.id === nic.id);
  const [config] = network.ipConfigurations;
  const publicIpInfo = await networkClient.publicIPAddresses.createOrUpdate(
    resourceGroup,
    `${prefix}-${vm.name}-ip`,
    {
      location: network.location,
      publicIPAllocationMethod: 'Static',
      publicIPAddressVersion: 'IPv4',
    }
  );
  return await networkClient.networkInterfaces.createOrUpdate(
    resourceGroup,
    `${prefix}-${network.name}`,
    {
      location: network.location,
      ipConfigurations: [
        {
          name: `${prefix}-${config.name}`,
          privateIPAllocationMethod: "Dynamic",
          subnet: config.subnet,
          publicIPAddress: publicIpInfo,
        },
      ],
    }
  );
}
```

We always assume that the `prefix` is something like a clone ID, while the template has a primary name. As an example, let's say the template VM is called `my-vm` with the network adapter `my-network` and the prefix is `clone42` then we'd end up with `clone42-my-network` for the network interface. The public IP address will be called `clone42-my-vm-ip`.

In total we have:

1. Public IP address (e.g., `clone42-my-vm-ip`)
2. Network adapter (e.g., `clone42-my-network`)
3. IP configuration, which attaches the IP address to the network adapter (e.g., `clone42-my-network-config`)

Similar, for the disk. Here, we choose the `osDisk` of the template VM as clone source. Important is the `createOption`, which can be set to `Copy`.

```js
async function createDisk(computeClient, vm, prefix) {
  const disk = vm.storageProfile.osDisk;
  return await computeClient.disks.createOrUpdate(
    resourceGroup,
    `${prefix}-${disk.name}`,
    {
      location: vm.location,
      creationData: {
        createOption: "Copy",
        sourceUri: disk.managedDisk.id,
      },
      sku: {
        name: disk.managedDisk.storageAccountType,
      },
      diskSizeGB: disk.diskSizeGB,
    }
  );
}
```

With these in mind we can actually write the cloning function. In short, it waits for the sub-resources to be created and then issues a new VM creation using the Azure REST API:

```js
async function cloneVirtualMachine(computeClient, networkClient, vm, prefix) {
  const cloneName = `${prefix}-${vm.name}`;

  const [disk, nic] = await Promise.all([
    createDisk(computeClient, vm, suffix),
    createNetwork(networkClient, vm, suffix),
  ]);
  const result = await computeClient.virtualMachines.createOrUpdate(
    resourceGroup,
    cloneName,
    {
      location: vm.location,
      plan: vm.plan,
      hardwareProfile: {
        vmSize: vm.hardwareProfile.vmSize,
      },
      networkProfile: {
        networkInterfaces: [
          {
            id: nic.id,
            primary: true,
          },
        ],
      },
      storageProfile: {
        osDisk: {
          createOption: "Attach",
          osType: vm.storageProfile.osDisk.osType,
          managedDisk: {
            id: disk.id,
          },
        },
      },
    }
  );

  return result;
}
```

Since we created the OS disk separately we only need to `Attach` the previously created resource. The great thing with the shown approach is that is really just takes the parameters from the template VM. So, if we want to change the VM plan or size we can do that on the template VM and then have all clones done correctly.

So much for the actual VM cloning, however, this is not all that we might need. Let's have a look at some little helpers that might come in handy.

## Little Helpers

Obviously, if we create we might want also to destroy. Having multiple clones sitting around doing nothing might not be ideal, which is why a delete functionality would be great.

Luckily, this is rather straight forward - the only thing to keep in mind is that the used sub-resources cannot be removed before the VM is removed. As a rule of thumb - resources can only be removed once no other resource has a dependency to it.

We therefore start by removing the actual VM followed by the disk and finally the network.

```js
async function deleteVirtualMachine(computeClient, networkClient, vm) {
  const [nic] = vm.networkProfile.networkInterfaces;
  const networks = await networkClient.networkInterfaces.list(resourceGroup);
  const network = networks.find((m) => m.id === nic.id);

  await computeClient.virtualMachines.deleteMethod(resourceGroup, vm.name);
  await computeClient.disks.deleteMethod(
    resourceGroup,
    vm.storageProfile.osDisk.name
  );
  await networkClient.networkInterfaces.deleteMethod(
    resourceGroup,
    network.name
  );
  await networkClient.publicIPAddresses.deleteMethod(
    resourceGroup,
    `${vm.name}-ip`
  );
}
```

This is great - and helps us to clean up properly.

Next, we require a function to actually turn a VM off or on. This is especially handy when we want to save money on the template VM. We would have it turned off all the time (except for updates / maintenance, of course) - only turning it on briefly for the cloning process.

**Remark** Turning off means *deallocating*. In Azure you can either turn off a VM (essentially still keeps the resources allocated / billing active) or deallocate it. The latter has to be done for saving money. The downside is that it definitely will take longer to restart it from this state.

```js
async function togglePower(computeClient, vm) {
  const running = await isRunning(computeClient, vm);

  if (running) {
    console.log('VM is running! Shutting down ...');
    await computeClient.virtualMachines.deallocate(resourceGroup, vm.name);
  } else {
    console.log('VM is shut down! Starting up ...');
    await computeClient.virtualMachines.start(resourceGroup, vm.name);
  }
  
  console.log('All done!');
}
```

Keep in mind that we use `deallocate` here. Alternatively, you could use `powerOff` to just suspend the VM (remember that you'd still be billed in that case).

In order to choose the right action (`deallocate` or `start`) we need a simple way of determining if the VM is running. The following snippet is helpful.

```js
async function isRunning(computeClient, vm) {
  const details = await computeClient.virtualMachines.get(
    resourceGroup,
    vm.name,
    {
      expand: "instanceView",
    }
  );

  return details.instanceView.statuses.some(
    (m) => m.code === "PowerState/running"
  );
}
```

More on these states can be found [in various online documentations](https://stefanos.cloud/blog/azure-vm-power-states-and-provisioning-states/). In short the state diagram for a VM looks as follows:

![State diagram by Stefanos Evangelou](https://i0.wp.com/stefanos.cloud/wp-content/uploads/2020/06/Azure-VM-Power-States.png?w=458&ssl=1)

Finally, in our use case a Windows VM has been created. The OS disk had an additional user account in there, which should receive a randomized password.

We can use the `runCommand` functionality to actually achieve this. The following snippet can reset the password of a local Windows user on the VM OS disk given a username `user` and a new password `newPassword`.

```js
async function changePassword(computeClient, vm, user, newPassword) {
  const res = await computeClient.virtualMachines.runCommand(
    resourceGroup,
    vm.name,
    {
      commandId: "RunPowerShellScript",
      script: [
        `Set-LocalUser -Name "${user}" -Password (ConvertTo-SecureString "${newPassword}" -AsPlainText -Force)`,
      ],
    }
  );
  const output = res.properties.output.value;
  return (
    output.some((m) => m.code === "ComponentStatus/StdOut/succeeded") &&
    output.some((m) => m.code === "ComponentStatus/StdErr/succeeded")
  );
}
```

Another thing that you might want to consider is a simple function to generate an RDP file. RDP is the remote desktop protocol and makes it possible to connect to a (Windows) VM from another computer. There is an integrated RDP client in Windows - on Mac OS the Microsoft Remote Desktop Client exists. Linux has some fantastic options, too.

```js
async function getRdpConnectionFile(networkClient, vm, user) {
  const network = await networkClient.publicIPAddresses.get(
    resourceGroup,
    `${vm.name}-ip`
  );
  return [
    `full address:s:${network.ipAddress}:3389`,
    `username:s:${user}`,
    `prompt for credentials:i:0`,
    `administrative session:i:0`,
  ].join("\n");
}
```

This generates a new file that automatically connects to the VM's public IP address using the given username.

## Conclusion

In this article I've showed you how you can leverage Node.js to clone a VM in Azure programmatically. This can be very in handy in many situations and allows you to tailor the process exactly to your needs.

The Azure REST API provides a very stable and intuitive interface to control all functionality around VMs. This makes it easy to write reliable scripts like the one above. In my own tests I've never encountered issues of any kind, even though the code above would still require retries and state management for edge case scenarios.