> For the complete documentation index, see [llms.txt](https://docs.expel.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.expel.io/connect-your-technology/expel-assembler/deploy-the-virtual-machine/deploy-the-assembler-in-microsoft-azure-terraform.md).

# Deploy the Assembler in Microsoft Azure (Terraform)

Each assembler you created must be deployed via a virtual machine, and then you can add your technology as a security device in Workbench to complete the full integration. For more information about the Expel Assembler or how it works, see the [About the Expel Assembler](/connect-your-technology/expel-assembler/about-the-expel-assembler.md) guide.

{% hint style="info" %}
Terraform lets you define both cloud and on-prem resources in human-readable configuration files that you can version, reuse, and share. These instructions assume you have working knowledge of Terraform; if you need additional help or context, refer to the [Terraform Documentation](https://developer.hashicorp.com/terraform).
{% endhint %}

## Prerequisites

1. You must have completed all of the steps in [Add a New Assembler](/connect-your-technology/expel-assembler/add-a-new-assembler.md) for each assembler you wish to deploy.
2. You must extract the Fedora CoreOS image file you [downloaded](/connect-your-technology/expel-assembler/add-a-new-assembler.md#step-3-download-the-base-coreos-image-artifact). You will need to upload the .vhd file, not the compressed .xz file, to use Terraform.
3. Make sure your network security group (for your [firewall configuration](/connect-your-technology/expel-assembler/add-a-new-assembler.md#step-2-update-your-firewall-configuration)) is in a [resource group](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-portal), and that you know which group it is. You will need to place the storage account, blob storage container, image, and virtual machine from this guide in that same resource group.
4. You must have [Terraform installed](https://developer.hashicorp.com/terraform/tutorials/gcp-get-started/install-cli?in=terraform%2Fgcp-get-started).

## Step 1: Download the Ignition File

The ignition file enables the virtual machine to read a configuration file, and to provision the Fedora CoreOS system based on the contents of that file. You will use this file when you configure the virtual machine in GCP.

1. [Log in to Workbench](https://workbench.expel.io/auth/login?orig=%2F).
2. In the side menu, navigate to **Organization Settings > Assemblers**.
3. Find the assembler you created, leave the file format as JSON, and select **Download the CoreOS Ignition File**. This action will download a JSON file that you will need in the next section.&#x20;

{% hint style="info" %}
You may choose a different file format if you like, but the JSON format is recommended for this type of assembler.
{% endhint %}

<figure><img src="/files/Eafkj0BJOEqa4x4PfyJF" alt="Select the JSON format in the right menu, and then select the download link."><figcaption></figcaption></figure>

4. Move your ignition file to a remote, secure location such as [Google's Cloud Storage](https://cloud.google.com/storage?hl=en). The contents of the ignition file will be stored in plaintext (unencrypted) wherever your Terraform state files are located.

{% hint style="danger" %}

1. **Do not store your ignition file in a git repository.** The file contains sensitive information and git is not a suitable place for this type of data.
2. **Be sure to lock down access to the storage location.** Only people who need access to the ignition file (and to the Terraform state files if using [Terraform Remote State](https://developer.hashicorp.com/terraform/language/state/remote)) should have access to the storage location.
   {% endhint %}

5) Repeat this process for any additional assemblers.&#x20;

{% hint style="warning" %}
You must keep track of which files came from which assembler, because each assembler has its own unique ignition file.
{% endhint %}

## Step 2: Install and Log In via the Azure CLI

You must authenticate Terraform with your Azure account. The first step is to get the CLI and log into your Azure account.

1. If you have not yet installed the Azure CLI tool, use one of the following links for instructions:
   * [Windows](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-windows?tabs=azure-cli)
   * [Linux](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-linux?pivots=apt)
   * [MacOS](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-macos)
2. Log into Azure via the CLI. This command will open a browser window and have you log in there (look for a successful login message in your browser, then go back to Terminal):

```
az login --scope https://graph.microsoft.com//.default
```

3. Terminal will retrieve and return your subscription and tenant information. Follow the instructions to select your subscription and tenant.

## Step 3: Set Up the Terraform Config File

You need to set up the config file so that Terraform uses the Azure provider to configure your infrastructure.

{% hint style="info" %}
See the[ Azure documentation](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs) if you need additional help with this step.&#x20;
{% endhint %}

1. Create a [Terraform config file](#user-content-fn-1)[^1], if you do not have one already.
2. Add the following configuration to your Terraform config file:

```
# We strongly recommend using the required_providers block to set the
# Azure provider source and the version being used
terraform {
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      version = "=3.0.0"
    }
  }
}

provider "azurerm" {
  features {}
}
```

## Step 4: Prepare the Resource Definitions File

You need a resource definitions file to hold your Terraform resources, SAS token, custom data,  network, internal subnet, and internal network interface.

1. Create a [resource definitions file](#user-content-fn-2)[^2], if you do not have one already.
2. Do one of the following:
   * If you do not have an existing storage account and blob storage container, and need to create them, continue to [Step 5: Create an Azure Storage Container](#step-5-create-an-azure-storage-container).
   * If you already have an existing storage account and blob storage container you want to use, and it is within the resource group that contains your network security group, skip to [Step 6: Upload the Ignition File and CoreOS File](#step-6-upload-the-ignition-file-and-coreos-file).

{% hint style="warning" %}
If you are deploying more than one assembler and the location of each assembler differs, you must use a different storage account and blob storage container for each location.
{% endhint %}

## Step 5: Create an Azure Storage Container

{% hint style="info" %}
If you already have an existing storage account and blob storage container you want to use within your resource group, and your assembler(s) are in **one location**, you should skip this step and [go to Step 6](#step-6-upload-the-ignition-file-and-coreos-file).
{% endhint %}

If you are deploying more than one assembler, you may use the same resources as long as all assemblers are in the same location.

1. To create these two resources quickly, add the following block to your resource definitions file.
   * Make sure to use the resource group that contains your network security group and firewall configuration for `YOUR_RESOURCE_GROUP_NAME`.
   * Make sure your `location` matches the location of your virtual machine. In this example, we have set the value to "East US".
   * You may use any name you like for your storage account (`YOUR_STORAGE_ACCOUNT_NAME`) and blob storage container (`YOUR_STORAGE_CONTAINER_NAME`).

{% hint style="warning" %}
If the location of each assembler differs, you must create a new storage account and blob storage container for each location.
{% endhint %}

```
resource "azurerm_resource_group" "assembler-resource-group" {
  name     = "YOUR_RESOURCE_GROUP_NAME"
  location = "East US"
}

resource "azurerm_storage_account" "assembler-storage-account" {
  name                     = "YOUR_STORAGE_ACCOUNT_NAME"
  resource_group_name      = azurerm_resource_group.assembler-resource-group.name
  location                 = azurerm_resource_group.assembler-resource-group.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_storage_container" "assembler-storage-container" {
  name                  = "YOUR_STORAGE_CONTAINER_NAME"
  storage_account_name  = azurerm_storage_account.assembler-storage-account.name
  container_access_type = "private"
```

2. Deploy your new Terraform resources.
   * Run `terraform init` to initialize the working directory.
   * Run `terraform plan` and review the changes.
   * If the plan looks right, run `terraform apply` and confirm the actions.

## Step 6: Upload the Ignition File and CoreOS File

Your CoreOs image file and ignition file (stored in [its remote location](#step-1-download-the-ignition-file)) can now be uploaded to your new container through the Azure portal.

{% hint style="info" %}
If you prefer using the Azure CLI, see [Upload a VHD to Azure or copy a managed disk to another region - Azure CLI](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/disks-upload-vhd-to-managed-disk-cli).&#x20;
{% endhint %}

1. [Log in to your Azure portal via a browser](https://portal.azure.com/).
2. Select **Storage Accounts**.
3. Select the storage account you created in the previous section, or that you already have and want to use.
4. Select **Upload**.
5. In the Upload blob window:
   * Drag-and-drop or browse for the **.vhd file**. *Do not upload the compressed .xz file.*
   * Find and select the container you created in the previous step, or that you already have and want to use.
   * Leave all **Advanced settings** as is.
   * Select **Upload**.
   * Drag and drop or browse for the **ignition file**.
   * Verify you are still in the same container.
   * Leave all **Advanced settings** as is.
   * Select **Upload**.
   * Repeat this process for any additional assemblers that are using that container.
   * Close the window.

## Step 7: Create a Managed Image in Azure

You must create a managed image that can be used for the Linux virtual machine.&#x20;

1. Still in the Azure portal, search for or select **Images**.
2. Select **Create**.
3. On the Create an image screen:
   * **Subscription** - select the subscription you want to use.
   * **Resource group** - select the resource group that contains your network security group; this is the same resource group that you chose to hold your storage container.
   * **Name** - enter a name for the image, such as "assemblercoreosimage".
   * **Region** - select the region for your resource group.
   * **Zone resiliency** - leave **unchecked**.
   * **OS type** - select **Linux**.
   * **VM generation** - select **Gen 1**.
   * **Storage blob** - use the **Browse** link to select the storage account you created in [Step 5](#step-5-create-an-azure-storage-container), then select the storage container, then select the .vhd file, then choose **Select**.
   * **Account type** - select **Standard SSD**.
   * **Host caching** - select **Read/write**.
   * **Key management** - select **Platform-managed key**.
   * **Data disk** - do not add a data disk.
4. Select **Review + Create**.
5. Review your configuration if desired, then select **Create**.

Before you move to the next step, you must obtain the URI for the image file (you will need it later in this guide). To do so:

1. From your list of images, select the image you just uploaded.
2. While viewing the image's Overview page, copy the **full URL** out of your browser and into a text editor.
3. Select the portion of the URL from “/subscriptions” all the way to the name of the image, and leave out “/overview” at the end. Save this portion as your image file's URI.

{% hint style="info" %}
**Example full URL (image name is"mycoreosimage"):**

`https://portal.azure.com/lab.onmicrosoft.com/resource/subscriptions/1abc2d34-e5f6-789g-h0i1-j2k345l6789m/resourceGroups/coresosresource/providers/Microsoft.Compute/images/mycoreosimage/overview`&#x20;

**Example resulting URI:**

`/subscriptions/1abc2d34-e5f6-789g-h0i1-j2k345l6789m/resourceGroups/coresosresource/providers/Microsoft.Compute/images/mycoreosimage`
{% endhint %}

## Step 8: Create a Shared Access Signature (SAS) for the Ignition File Blob

To give CoreOS the ability to remotely access the ignition file from your Azure storage container, you must provide a link to the file.&#x20;

{% hint style="info" %}
If you are deploying more than one assembler, this step will need to be repeated for each ignition file.
{% endhint %}

Add the following block to your resource definitions file to create your SAS.

* If you are using a pre-existing storage account that is not hosted by Terraform, be sure to update the `primary_connection_string` attribute accordingly.&#x20;
* If you need help finding the string in your Azure portal, follow the steps in this [Microsoft Help Forum topic](https://learn.microsoft.com/en-us/answers/questions/1071173/where-can-i-find-storage-account-connection-string).&#x20;

{% hint style="warning" %}
This step creates a [Shared Access Signature (SAS Token)](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/storage_account_sas) with an expiration of one hour. Make sure to complete the rest of this onboarding guide before it expires.
{% endhint %}

```
data "azurerm_storage_account_sas" "assembler-container" {
  connection_string = azurerm_storage_account.assembler-storage-account.primary_connection_string
  https_only        = true
  start             = timestamp()
  expiry            = timeadd(timestamp(), "1h")
  signed_version = "2019-10-10"
  resource_types {
    service = false
    container = true
    object = true
  }
  services {
    blob = true
    queue = false
    table = false
    file = true
    }
    permissions {
        read = true
        write = false
        delete = false
        list = false
        add = false
        create = false
        update = false
        process = false
    }
}
```

## Step 9: Create the Custom Data via Terraform’s `Template_File` Source

This step creates the file used in a later step’s `custom_data` resource attribute.&#x20;

{% hint style="info" %}
If you are deploying more than one assembler, this step will need to be repeated for each assembler's ignition file.
{% endhint %}

Add the following block to your resource definitions file to create your custom data.

* Be sure to replace `NAME-OF-BLOB` with the file name of the storage blob for your ignition file.

```
data "template_file" "custom_data" {
  template = jsonencode({
    ignition = {
      config = {
        replace = {
          source = "${azurerm_storage_account.assembler-storage-account.primary_blob_endpoint}${azurerm_storage_container.assembler-storage-container.name}/NAME-OF-BLOB${data.azurerm_storage_account_sas.assembler-container.sas}"
        }
      },
      version = "3.4.0"
    }
  })
}
```

## Step 10: Configure Your VM's Network Infrastructure

{% hint style="info" %}
If you already have an existing network infrastructure to place your Assembler's virtual machine within, you may skip this step and go to [Step 11](#step-11-configure-and-spin-up-the-virtual-machine).
{% endhint %}

This Terraform block creates a [network](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network), an internal [subnet](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet), and an internal [network interface](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_interface) for use by the Assembler's virtual machine. Add it to your resource definitions file and be sure to specify your own:

* Network name (`YOUR_NETWORK_NAME`)
* Internal subnet name (`YOUR_SUBNET_NAME`)
* Internal network interface name (`YOUR_NETWORK_INTERFACE_NAME`)

```
resource "azurerm_virtual_network" "assembler-network" {
  name                = "YOUR_NETWORK_NAME"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.assembler-resource-group.location
  resource_group_name = azurerm_resource_group.assembler-resource-group.name
}

resource "azurerm_subnet" "assembler-subnet" {
  name                 = "YOUR_SUBNET_NAME"
  resource_group_name  = azurerm_resource_group.assembler-resource-group.name
  virtual_network_name = azurerm_virtual_network.assembler-network.name
  address_prefixes     = ["10.0.2.0/24"]
}

resource "azurerm_network_interface" "assembler-network-interface" {
  name                = "YOUR_NETWORK_INTERFACE_NAME"
  location            = azurerm_resource_group.assembler-resource-group.location
  resource_group_name = azurerm_resource_group.assembler-resource-group.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.assembler-subnet.id
    private_ip_address_allocation = "Dynamic"
  }
}
```

## Step 11: Configure and Spin Up the Virtual Machine

{% hint style="info" %}

* Before you begin, make sure you have your URI available ([Step 7](#step-7-create-a-managed-image-in-azure)).
* If you are deploying more than one assembler, repeat this process for each assembler and be sure to use a unique name for each one.
  {% endhint %}

A few notes about this block:

* The `size` is “Standard\_D2\_v31”, which has the minimum CPU and RAM requirements for an assembler.&#x20;
* The `disk_size_gb` is 20, which indicates the 20GB minimum disk size required for an assembler.
* The `admin_username`, `username`, and `admin_ssh_key` have been filled in for you. The Azure Terraform Provider requires values in these fields, but they will be overwritten by the Assembler bootup sequence.

To configure your virtual machine:&#x20;

1. Add the following block to your resource definitions file. This will instruct the [azurerm\_linux\_virtual\_machine](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/linux_virtual_machine) resource to configure your virtual machine with the minimum requirements. Make sure to:
   * Use a unique `name` for each assembler. If you are just deploying one assembler, you can leave the name value as "assembler".
   * Add your image URI (from [Step 7](#step-7-create-a-managed-image-in-azure)) as the `source_image_id`.

```
resource "azurerm_linux_virtual_machine" "assembler" {
  name                = "assembler"
  resource_group_name = azurerm_resource_group.assembler-resource-group.name
  location            = azurerm_resource_group.assembler-resource-group.location
  size                = "Standard_D2_v31"
  # admin_username is required. However, the assembler installation replaces all users on the machine
  admin_username      = "customer"
  network_interface_ids = [
    azurerm_network_interface.assembler-network-interface.id,
  ]

  custom_data = base64encode(data.template_file.custom_data.rendered)

  # admin_ssh_key is required. However, the assembler installation replaces all ssh keys on the machine.
  admin_ssh_key {
    username   = "customer"
    public_key = "AAA123BBB456CCC789"
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
    disk_size_gb         = 20
  }

 source_image_id = "YOUR_IMAGE_URI"
}
```

2. Deploy your Terraform resources.
3. Run `terraform init` to initialize the working directory.
4. Run `terraform plan` and review the changes.
5. If the plan looks right, run `terraform apply` and confirm the actions.

## Step 12: Verify a “Connected” Status in Workbench

{% hint style="info" %}
It can take 10 to 15 minutes for the assembler’s status to update in Workbench.
{% endhint %}

1. [Log in to Workbench](https://workbench.expel.io/auth/login?orig=%2F).
2. In the side menu, navigate to **Organization Settings > Assemblers** (or, refresh the page if you never logged out).
3. Find your newly created assembler(s) and verify that the status has changed from “Not Yet Connected” to “Connected.”
   * If your assembler(s) still do not show as connected after waiting at least 15 minutes and refreshing the page, see the [Troubleshooting](#troubleshooting) section for help.

## Step 13: Connect Your Technology

You can now set up your vendor technology in Workbench.

<button type="button" class="button primary" data-action="search" data-icon="magnifying-glass">Search for your setup guide…</button>

## Troubleshooting

If your assembler is still not showing as “Connected” after 15 minutes:

* Make sure your chosen connection has the [proper firewall configurations](/connect-your-technology/expel-assembler/add-a-new-assembler.md#step-2-update-your-firewall-configuration) to allow our outbound ports.
* Make sure your config file includes the correct region ([Step 3](#step-3-set-up-the-terraform-config-file)).
* Make sure your ignition file is at the path specified, and that you are referencing the correct ignition file for your assembler ([Step 1](#step-1-download-the-ignition-file)).
* Make sure your chosen machine’s size meets the required minimums (2 virtual CPUs, 8 GB RAM, and 20 GB disk space).
* Make sure the login credentials you obtained for the assembler are for a user who has admin permissions in Workbench.
* If Boot Diagnostics are available on the machine, go to the assembler’s virtual machine page in the Azure Portal and select **Help**. Then select **Boot Diagnostics** to see a screenshot of the serial console. Or, connect to the serial console by selecting **Serial Console** and entering your assembler’s customer credentials (this requires Workbench admin access). These places may help diagnose the problem.

If all firewall, config file, and resource definitions settings are correct and you are still unable to connect the assembler, [contact Support](/support/how-to-reach-us.md) for help.

## Reference

### Full Code Examples

<details>

<summary>terraform.tf (config file)</summary>

```
# We strongly recommend using the required_providers block to set the
# Azure provider source and the version being used
terraform {
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      version = "=3.0.0"
    }
  }
}

provider "azurerm" {
  features {}
}
```

</details>

<details>

<summary>assembler.tf (resource definitions)</summary>

```
resource "azurerm_resource_group" "assembler-resource-group" {
  name     = "YOUR_RESOURCE-GROUP-NAME"
  location = "East US"
}

resource "azurerm_storage_account" "assembler-storage-account" {
  name                     = "YOUR_STORAGE_ACCOUNT_NAME"
  resource_group_name      = azurerm_resource_group.assembler-resource-group.name
  location                 = azurerm_resource_group.assembler-resource-group.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_storage_container" "assembler-storage-container" {
  name                  = "YOUR_CONTAINER_NAME"
  storage_account_name  = azurerm_storage_account.assembler-storage-account.name
  container_access_type = "private"
}

data "azurerm_storage_account_sas" "assembler-container" {
  connection_string = azurerm_storage_account.assembler-storage-account.primary_connection_string
  https_only        = true
  start             = timestamp()
  expiry            = timeadd(timestamp(), "1h")
  signed_version = "2019-10-10"
  resource_types {
    service = false
    container = true
    object = true
  }
  services {
    blob = true
    queue = false
    table = false
    file = true
    }
    permissions {
        read = true
        write = false
        delete = false
        list = false
        add = false
        create = false
        update = false
        process = false
    }
}

data "template_file" "custom_data" {
  template = jsonencode({
    ignition = {
      config = {
        replace = {
          source = "${azurerm_storage_account.assembler-storage-account.primary_blob_endpoint}${azurerm_storage_container.assembler-storage-container.name}/NAME-OF-BLOB${data.azurerm_storage_account_sas.assembler-container.sas}"
        }
      },
      version = "3.4.0"
    }
  })
}

resource "azurerm_virtual_network" "assembler-network" {
  name                = "YOUR_NETWORK_NAME"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.assembler-resource-group.location
  resource_group_name = azurerm_resource_group.assembler-resource-group.name
}

resource "azurerm_subnet" "assembler-subnet" {
  name                 = "YOUR_SUBNET_NAME"
  resource_group_name  = azurerm_resource_group.assembler-resource-group.name
  virtual_network_name = azurerm_virtual_network.assembler-network.name
  address_prefixes     = ["10.0.2.0/24"]
}

resource "azurerm_network_interface" "assembler-network-interface" {
  name                = "YOUR_NETWORK_INTERFACE_NAME"
  location            = azurerm_resource_group.assembler-resource-group.location
  resource_group_name = azurerm_resource_group.assembler-resource-group.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.assembler-subnet.id
    private_ip_address_allocation = "Dynamic"
  }
}

resource "azurerm_linux_virtual_machine" "assembler" {
  name                = "assembler"
  resource_group_name = azurerm_resource_group.assembler-resource-group.name
  location            = azurerm_resource_group.assembler-resource-group.location
  size                = "Standard_D2_v31"
  # admin_username is required. However, the assembler installation replaces all users on the machine
  admin_username      = "customer"
  network_interface_ids = [
    azurerm_network_interface.assembler-network-interface.id,
  ]

  custom_data = base64encode(data.template_file.custom_data.rendered)

  # admin_ssh_key is required. However, the assembler installation replaces all ssh keys on the machine.
  admin_ssh_key {
    username   = "customer"
    public_key = "AAA123BBB456CCC789"
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
    disk_size_gb         = 20
  }

  source_image_id = "YOUR_IMAGE_URI"
}
```

</details>

[^1]: A file name might be terraform.tf.

[^2]: A file name might be assembler.tf.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.expel.io/connect-your-technology/expel-assembler/deploy-the-virtual-machine/deploy-the-assembler-in-microsoft-azure-terraform.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
