# Manual CLI Setup

This article walks through running Archera's onboarding script manually via the Azure CLI. Use this when the in-app automated onboarding flow cannot be used — for example, if your organisation requires pre-approved scripts or has restrictions on OAuth-based provisioning.

The script is interactive: it prompts for your Tenant ID and a management subscription, lets you pick which subscriptions to onboard from a checklist, asks for confirmation before creating any resources, and prints a summary when complete.

## Choosing Your Environment

{% tabs %}
{% tab title="Azure Cloud Shell (recommended)" %}
Cloud Shell runs in your browser inside the Azure Portal and is pre-authenticated — no `az login` required.

1. Open the [Azure Portal](https://portal.azure.com)
2. Click the **Cloud Shell** icon (`>_`) in the top navigation bar
3. Select **Bash** when prompted
4. Proceed to [Download and Run the Script](#download-and-run-the-script)
   {% endtab %}

{% tab title="Local Azure CLI" %}
If running locally:

1. Install the [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli)
2. Install whiptail: `sudo apt-get install -y whiptail` (Linux) or `brew install newt` (macOS)
3. Sign in: `az login --tenant <your-tenant-id>`
4. Proceed to [Download and Run the Script](#download-and-run-the-script)
   {% endtab %}
   {% endtabs %}

## Prerequisites

* Your **Tenant ID** (see [How Do I Find My Azure Tenant and Subscription IDs?](https://github.com/reserved-ai/archera-public-docs/blob/main/public/HelpCenter/azure-onboarding/technical-onboarding/tenant-subscription-ids.md))
* A **management subscription ID** — the subscription where Archera's resource group and storage account will be created. This is typically a shared services or production subscription.
  * You will be able to choose additional subscriptions to onboard into Archera later in this script.
* The onboarding user must have:
  * **Owner** (or equivalent) on the management subscription
  * **Access management for Azure resources** enabled in Microsoft Entra ID (required to create custom roles at tenant scope — see [Prerequisite Checklist](/help-center/azure-onboarding/technical-onboarding/prerequisite-checklist.md))
* **whiptail** installed (pre-installed in Azure Cloud Shell and most Linux distributions)

## What the Script Does

1. Validates your tenant ID and management subscription
2. Registers the `Microsoft.Storage` and `Microsoft.CostManagementExports` resource providers
3. Creates the Archera custom RBAC role at tenant scope (skips if it already exists)
4. Presents a checklist of eligible subscriptions — select the ones to onboard
5. Assigns the custom role to each selected subscription
6. Creates a resource group and storage account in the management subscription to receive Azure Cost Management exports
7. Assigns the required built-in roles to the Archera Enterprise Application
8. Prints a completion summary with all resource names and IDs

## Supported Subscription Types

The script only shows subscriptions eligible for FOCUS cost exports:

| Quota ID                         | Plan                    |
| -------------------------------- | ----------------------- |
| `PayAsYouGo_2014-09-01`          | Pay-As-You-Go           |
| `EnterpriseAgreement_2014-09-01` | Enterprise Agreement    |
| `CSP_2015-05-01`                 | Cloud Solution Provider |
| `CSP_MG_2017-12-01`              | CSP (Management Group)  |
| `MSDNDevTest_2014-09-01`         | MSDN / Dev-Test         |

Free Trial, Sponsored, Student, and other plan types do not support cost exports and will not appear in the checklist.

## Run the Script

Download the script or copy it below, save it as `azure-cli.sh`, and run it:

```bash
bash azure-cli.sh
```

{% file src="/files/CEQLn3uIisFNQWqtKjCn" %}

{% code title="azure-cli.sh" %}

```bash
#!/usr/bin/env bash
# Archera onboarding for Azure
#
# This script grants Archera (https://archera.ai) the access it needs to read your
# Azure usage and commitment data, and to manage Reservations / Savings Plans on
# your behalf. It will:
#   1. Provision the Archera enterprise app in your tenant (one-time consent).
#   2. Create a tenant-scope custom role with read-only billing/usage permissions.
#   3. Create a resource group and storage account to hold cost exports.
#   4. Assign the custom role + a few built-in Azure roles to the Archera app.
#
# You'll be prompted to:
#   - Confirm your tenant ID and the subscription that will host the storage
#     account ("management subscription").
#   - Pick which subscriptions to onboard from the eligible list.
#   - Approve resource creation before anything is provisioned.
#
# Prerequisites:
#   - Azure CLI: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli
#   - whiptail (preinstalled on most Linux; macOS: `brew install newt`)
#   - You must be signed in: `az login --tenant <your-tenant-id>`
#   - Your account needs:
#       * Owner (or equivalent) on the management subscription
#       * Permission to create custom roles at tenant root (User Access
#         Administrator at the root management group — typically requires the
#         "Access management for Azure resources" toggle in Microsoft Entra ID).
#
# Supported tenant types: direct subscription onboarding only (Pay-As-You-Go,
# Enterprise Agreement, CSP, MSDN/DevTest). Microsoft Customer Agreement billing
# account onboarding is not supported by this script.
#
# Questions: contact your Archera support representative.

set -euo pipefail

main() {
  # Dependencies
  command -v az >/dev/null || { echo "az CLI not installed (https://learn.microsoft.com/en-us/cli/azure/install-azure-cli)"; exit 1; }
  command -v whiptail >/dev/null || { echo "whiptail not installed (install via 'brew install newt' on macOS or 'apt-get install whiptail' on Linux)"; exit 1; }

  # User inputs and validation
  read -rp "Tenant ID: " TENANT_ID
  read -rp "Management subscription ID: " MANAGEMENT_SUB_ID
  read -rp "Azure region [eastus]: " REGION
  REGION="${REGION:-eastus}"
  validate_inputs

  # Pin az session to the management subscription
  az account set --subscription "$MANAGEMENT_SUB_ID"

  # Default resource names (recommended)
  SUFFIX="${MANAGEMENT_SUB_ID%%-*}"
  ROLE_NAME="archerarole${SUFFIX}"
  RG="archeraresource${SUFFIX}"
  STORAGE="archerastorage${SUFFIX}"
  STORAGE_SCOPE="/subscriptions/${MANAGEMENT_SUB_ID}/resourceGroups/${RG}/providers/Microsoft.Storage/storageAccounts/${STORAGE}"

  # Create new or fetch existing Archera.ai Enterprise Application
  echo "Resolving Archera enterprise application..."
  ARCHERA_APP_CLIENT_ID="f6e3844b-18dc-479d-ad28-4359dd5b7286"
  ARCHERA_APP=$(az ad sp show --id "$ARCHERA_APP_CLIENT_ID" --query id -o tsv 2>/dev/null \
    || az ad sp create --id "$ARCHERA_APP_CLIENT_ID" --query id -o tsv)
  [[ -n "$ARCHERA_APP" ]] || { echo "Could not resolve Archera SP. Ensure admin consent is granted in tenant $TENANT_ID."; exit 1; }

  # Select eligible subscriptions
  select_subscriptions

  # Show full plan and ask for one approval before any provisioning
  echo
  echo "===================================================================="
  echo "  About to apply the following changes:"
  echo "===================================================================="
  echo "  Tenant ID:               $TENANT_ID"
  echo "  Management subscription: $MANAGEMENT_SUB_ID"
  echo "  Region:                  $REGION"
  echo "  Custom role:             $ROLE_NAME (tenant scope)"
  echo "  Resource group:          $RG"
  echo "  Storage account:         $STORAGE"
  echo "  Custom role assigned to subscriptions:"
  printf "    - %s\n" "${SUBSCRIPTIONS[@]}"
  echo "  Built-in roles assigned to Archera SP:"
  echo "    - Reader and Data Access, Storage Blob Data Reader, User Access Administrator (storage scope)"
  echo "    - Reservation Reader / Purchaser (capacity + management sub)"
  echo "    - Savings Plan Reader / Purchaser (billingbenefits + management sub)"
  echo "    - Advisor Recommendations Contributor (advisor scope)"
  echo "===================================================================="
  confirm "Proceed?"

  # Register resource providers
  echo "Registering Microsoft.Storage provider (may take ~15 seconds)..."
  az provider register --namespace Microsoft.Storage --wait
  echo "Registering Microsoft.CostManagementExports provider (may take ~15 seconds)..."
  az provider register --namespace Microsoft.CostManagementExports --wait

  # Custom role and per-subscription assignment
  create_custom_role
  for sub_id in "${SUBSCRIPTIONS[@]}"; do
    assign_role "$ROLE_NAME" "/subscriptions/${sub_id}"
  done

  # Resource group and storage account
  az group create --name "$RG" --location "$REGION"
  echo "Creating storage account '$STORAGE' (this may take up to 30 seconds)..."
  local sa_output sa_rc=0
  sa_output=$(az storage account create \
    --name "$STORAGE" --resource-group "$RG" --location "$REGION" \
    --sku Standard_LRS --kind StorageV2 \
    --min-tls-version TLS1_2 \
    --allow-blob-public-access false \
    --require-infrastructure-encryption true \
    --encryption-services blob file 2>&1) || sa_rc=$?
  echo "$sa_output"
  if [[ $sa_rc -ne 0 ]]; then
    if grep -q -E "msrestazure|No module named" <<< "$sa_output"; then
      echo
      echo "This is a known issue with the 'storage-preview' az extension."
      echo "Fix: az extension remove --name storage-preview"
      echo "Then re-run this script."
    fi
    exit "$sa_rc"
  fi

  # Storage role assignments
  echo "Assigning roles (this may take a moment)..."
  READER_AND_DATA_ACCESS_ROLE_ID="c12c1c16-33a1-487b-954d-41c89c60f349"
  STORAGE_BLOB_DATA_READER_ROLE_ID="2a2b9908-6ea1-4ae2-8e65-a410df84e7d1"
  USER_ACCESS_ADMIN_ROLE_ID="18d7d88d-d35e-4fb5-a5c3-7773c20a72d9"
  assign_role "$READER_AND_DATA_ACCESS_ROLE_ID" "$STORAGE_SCOPE"
  assign_role "$STORAGE_BLOB_DATA_READER_ROLE_ID" "$STORAGE_SCOPE"
  assign_role "$USER_ACCESS_ADMIN_ROLE_ID" "$STORAGE_SCOPE"

  # Reservations role assignments
  RESERVATION_SCOPE="/providers/Microsoft.Capacity"
  RESERVATION_READER_ROLE_ID="582fc458-8989-419f-a480-75249bc5db7e"
  RESERVATION_PURCHASER_ROLE_ID="f7b75c60-3036-4b75-91c3-6b41c27c1689"
  assign_role "$RESERVATION_READER_ROLE_ID" "$RESERVATION_SCOPE"
  assign_role "$RESERVATION_PURCHASER_ROLE_ID" "$RESERVATION_SCOPE"
  assign_role "$RESERVATION_PURCHASER_ROLE_ID" "/subscriptions/${MANAGEMENT_SUB_ID}"

  # Savings plans role assignments
  SAVINGSPLAN_SCOPE="/providers/Microsoft.BillingBenefits"
  SAVINGSPLAN_READER_ROLE_ID="d534ad90-4ac5-4815-a178-b2e47397baab"
  SAVINGSPLAN_PURCHASER_ROLE_ID="3d24a3a0-c154-4f6f-a5ed-adc8e01ddb74"
  assign_role "$SAVINGSPLAN_READER_ROLE_ID" "$SAVINGSPLAN_SCOPE"
  assign_role "$SAVINGSPLAN_PURCHASER_ROLE_ID" "$SAVINGSPLAN_SCOPE"
  assign_role "$SAVINGSPLAN_PURCHASER_ROLE_ID" "/subscriptions/${MANAGEMENT_SUB_ID}"

  # Advisor role assignments
  ADVISOR_SCOPE="/providers/Microsoft.Advisor"
  ADVISOR_RECOMMENDATIONS_CONTRIBUTOR_ROLE_ID="6b534d80-e337-47c4-864f-140f5c7f593d"
  assign_role "$ADVISOR_RECOMMENDATIONS_CONTRIBUTOR_ROLE_ID" "$ADVISOR_SCOPE"

  # Summary
  echo
  echo "===================================================================="
  echo "  Archera Azure onboarding complete"
  echo "===================================================================="
  echo "  Tenant ID:              $TENANT_ID"
  echo "  Management subscription: $MANAGEMENT_SUB_ID"
  echo "  Resource group:         $RG"
  echo "  Storage account:        $STORAGE"
  echo "  Custom role:            $ROLE_NAME"
  echo "  Onboarded subscriptions:"
  printf "    - %s\n" "${SUBSCRIPTIONS[@]}"
  echo
  echo "  Next step: notify your Archera support contact that onboarding is"
  echo "  complete and share the tenant + subscription IDs above."
  echo "===================================================================="
}

confirm() {
  local reply
  read -rp "$1 [y/Y]: " reply
  [[ "$reply" == "y" || "$reply" == "Y" ]] || { echo "Aborted."; exit 1; }
}

# Validates $TENANT_ID and $MANAGEMENT_SUB_ID format, az session tenant, and sub-in-tenant.
validate_inputs() {
  local uuid_re='^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
  local active_tenant sub_tenant
  [[ "$TENANT_ID"        =~ $uuid_re ]] || { echo "TENANT_ID must be a lowercase UUID, got: '$TENANT_ID'"; exit 1; }
  [[ "$MANAGEMENT_SUB_ID" =~ $uuid_re ]] || { echo "MANAGEMENT_SUB_ID must be a lowercase UUID, got: '$MANAGEMENT_SUB_ID'"; exit 1; }
  active_tenant=$(az account show --query tenantId -o tsv 2>/dev/null || true)
  [[ "$TENANT_ID" == "$active_tenant" ]] || { echo "TENANT_ID '$TENANT_ID' does not match active az session tenant '$active_tenant'. Run 'az login --tenant $TENANT_ID'."; exit 1; }
  sub_tenant=$(az account show --subscription "$MANAGEMENT_SUB_ID" --query tenantId -o tsv 2>/dev/null || true)
  [[ "$sub_tenant" == "$TENANT_ID" ]] || { echo "Subscription '$MANAGEMENT_SUB_ID' not found in tenant '$TENANT_ID'."; exit 1; }
}

# Idempotent role assignment for $ARCHERA_APP. Args: <role-id> <scope>
assign_role() {
  local role_id="$1" scope="$2" output rc=0
  output=$(az role assignment create --assignee-object-id "$ARCHERA_APP" --assignee-principal-type ServicePrincipal \
    --role "$role_id" --scope "$scope" 2>&1) || rc=$?
  if [[ $rc -eq 0 ]]; then
    return 0
  fi
  if grep -q -E "RoleAssignmentExists|already exists" <<< "$output"; then
    echo "Role $role_id already assigned at $scope — skipping."
    return 0
  fi
  echo "$output" >&2
  return $rc
}

# Creates the Archera custom role at tenant scope; skip-on-exists, never updates.
create_custom_role() {
  local description="Archera Custom Role v2"
  local actions='[
    "*/read",
    "Microsoft.Consumption/*",
    "Microsoft.CostManagement/*",
    "Microsoft.Billing/billingPeriods/read",
    "Microsoft.Billing/billingProperty/read",
    "Microsoft.Resources/subscriptions/read",
    "Microsoft.Resources/subscriptions/resourceGroups/read",
    "Microsoft.Advisor/configurations/read",
    "Microsoft.Advisor/recommendations/read",
    "Microsoft.Management/managementGroups/read"
  ]'
  local tenant_scope="/providers/Microsoft.Management/managementGroups/${TENANT_ID}"
  local existing_id role_def_json
  echo "Checking for existing custom role..."
  existing_id=$(az role definition list --name "$ROLE_NAME" --scope "$tenant_scope" --query "[0].id" -o tsv)
  if [[ -n "$existing_id" ]]; then
    echo "Custom role '$ROLE_NAME' already exists at $tenant_scope — skipping."
    return 0
  fi
  echo "Creating custom role at tenant scope (may take ~15 seconds)..."
  role_def_json=$(cat <<EOF
{
  "name": "${ROLE_NAME}",
  "roleName": "${ROLE_NAME}",
  "description": "${description}",
  "type": "CustomRole",
  "actions": ${actions},
  "notActions": [],
  "dataActions": [],
  "notDataActions": [],
  "assignableScopes": ["${tenant_scope}"]
}
EOF
)
  az role definition create --role-definition "$role_def_json"
}

# Discovers eligible subs (FOCUS-supported quota IDs, state=Enabled)
select_subscriptions() {
  local quotas="['PayAsYouGo_2014-09-01','EnterpriseAgreement_2014-09-01','CSP_2015-05-01','CSP_MG_2017-12-01','MSDNDevTest_2014-09-01']"
  local eligible=() line id name quota status checklist=() selected
  echo "Fetching eligible subscriptions..."
  while IFS= read -r line; do
    eligible+=( "$line" )
  done < <(az rest --method get \
    --url "https://management.azure.com/subscriptions?api-version=2020-01-01" \
    --query "value[?state=='Enabled' && contains(${quotas}, subscriptionPolicies.quotaId)].[subscriptionId, displayName, subscriptionPolicies.quotaId]" \
    -o tsv)

  [[ ${#eligible[@]} -gt 0 ]] || { echo "No eligible subscriptions found in tenant $TENANT_ID"; exit 1; }

  for line in "${eligible[@]}"; do
    IFS=$'\t' read -r id name quota <<< "$line"
    status="OFF"
    [[ "$id" == "$MANAGEMENT_SUB_ID" ]] && status="ON"
    checklist+=( "$id" "$name ($quota)" "$status" )
  done

  selected=$(whiptail --title "Archera Azure Onboarding" --separate-output \
    --checklist "Use 'Arrow Keys and Space Bar' to select, 'Tab Key' to OK/Cancel, 'Enter Key' to confirm." \
    0 0 "${#eligible[@]}" "${checklist[@]}" \
    3>&1 1>&2 2>&3) || { echo "Selection cancelled"; exit 1; }

  SUBSCRIPTIONS=()
  while IFS= read -r line; do
    SUBSCRIPTIONS+=( "$line" )
  done <<< "$selected"
  [[ ${#SUBSCRIPTIONS[@]} -gt 0 ]] || { echo "No subscriptions selected"; exit 1; }
}

main "$@"
```

{% endcode %}

The script will prompt you for:

1. **Tenant ID** — your Azure Tenant ID (UUID format)
2. **Management subscription ID** — the subscription to host the storage account
3. **Azure region** — where the resource group and storage account will be created (defaults to `eastus`)

It then validates your active `az` session matches the tenant, fetches eligible subscriptions, and presents the whiptail checklist. Use **SPACE** to toggle subscriptions, **TAB** to move to OK/Cancel, and **ENTER** to confirm.

You will be asked to confirm before any resources are created.

## Role Summary

| Role                                | Scope                                                            | Purpose                                                       |
| ----------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------- |
| Archera Custom Role (read)          | Each selected subscription                                       | Billing, cost management, resource reads                      |
| Reader and Data Access              | Storage account                                                  | SAS token generation for cost export reads                    |
| Storage Blob Data Reader            | Storage account                                                  | Direct blob read access                                       |
| User Access Administrator           | Storage account                                                  | Grant write access to Cost Management export managed identity |
| Reservation Reader                  | `/providers/Microsoft.Capacity`                                  | Read existing Reserved Instances                              |
| Reservation Purchaser               | `/providers/Microsoft.Capacity` + management subscription        | Purchase Reserved Instances                                   |
| Savings Plan Reader                 | `/providers/Microsoft.BillingBenefits`                           | Read existing Savings Plans                                   |
| Savings Plan Purchaser              | `/providers/Microsoft.BillingBenefits` + management subscription | Purchase Savings Plans                                        |
| Advisor Recommendations Contributor | `/providers/Microsoft.Advisor`                                   | Read Advisor recommendations                                  |

## Related Resources

* [Azure Onboarding - Prerequisite Checklist](/help-center/azure-onboarding/technical-onboarding/prerequisite-checklist.md)
* [Azure Onboarding - Required Permissions](/help-center/azure-onboarding/technical-onboarding/required-permissions.md)
* [How Does Archera Access My Azure Environment?](/help-center/security/azure-access.md)
* [Azure Offboarding](https://github.com/reserved-ai/archera-public-docs/blob/main/public/HelpCenter/azure-onboarding/technical-onboarding/offboarding.md)


---

# Agent Instructions: 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:

```
GET https://docs.archera.ai/help-center/azure-onboarding/technical-onboarding/manual-cli-setup.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
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.
