> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formae.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Install and run the agent on Azure

> Stand up a production formae agent on Azure with the formae-bootstrap installer, then connect your CLI to it.

formae runs as a client and an agent. The agent lives in your infrastructure,
executes changes, and keeps state in sync with your cloud; you use your local CLI
to provision it once, then [point the CLI at it](/documentation/guides/manage-profiles)
with a profile and hand off. See [Architecture](/documentation/concepts/architecture)
for the client/agent model.

The recommended path is the open-source
[`formae-bootstrap`](https://github.com/platform-engineering-labs/formae-bootstrap)
installer. One apply stands up the whole agent (a resource group, a VNet, a private
Azure Database for PostgreSQL Flexible Server, and a VM running the agent
container), secure by default.

<Note>
  This guide covers the Azure bootstrap path. For other clouds, see
  [Install on AWS](/documentation/guides/install-agent) and
  [Install on GCP](/documentation/guides/install-agent-gcp).
</Note>

## Choose an access mode

Compute is always a VM; `--access` decides how clients reach it. Basic auth is on
in every mode.

* **Public (`public`, default)**: the agent's own public IP, with the API port
  opened to `--allowed-cidr`. The agent terminates HTTPS itself with a self-signed
  certificate; the connect profile sets `insecureSkipVerify` so the CLI accepts it.
* **App Gateway (`appgw`)**: an Application Gateway v2 fronts the VM on `:443`,
  terminating TLS with a Key Vault certificate (self-signed by default, or your own
  PFX for a browser-trusted cert). The Azure analog of the AWS `alb` mode. Requires
  a domain you own.
* **Tailscale (`tailnet`)**: private, reachable only over your Tailscale tailnet;
  the agent serves a trusted `*.ts.net` certificate. No public ingress.

## Prerequisites

* The formae CLI, matching the agent image version. If you have not installed it,
  see the [Quick start](/documentation/get-started/quickstart).

* Local Azure credentials for the formae agent you run locally to perform the apply.
  The Azure plugin uses `DefaultAzureCredential`, so `az login` is enough (or set
  `AZURE_TENANT_ID` / `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET`). Restart the local
  agent after setting them.

* A service principal the remote agent operates Azure with:

  ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
  az ad sp create-for-rbac --name formae-agent --role Contributor \
    --scopes /subscriptions/<subscription-id>
  # note appId (--client-id), password (--client-secret), tenant (--tenant-id)
  ```

* An SSH public key (Azure requires one on the VM even though no inbound SSH is
  opened): `ssh-keygen -t ed25519` if you don't have one.

* App Gateway mode: a domain you control, a globally-unique Key Vault name (3 to 24
  chars), and the applying principal's objectId
  (`az ad sp show --id <client-id> --query id -o tsv`).

* Tailscale (tailnet mode only): a reusable auth key tagged `tag:formae`, with HTTPS
  certificates enabled on your tailnet.

Clone the installer:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
git clone https://github.com/platform-engineering-labs/formae-bootstrap.git
cd formae-bootstrap
```

## Install

<Tabs>
  <Tab title="Public (self-signed)">
    <Steps>
      <Step title="Generate the API credential">
        Basic auth validates a bcrypt hash, produced locally; the script also prints
        a stable Postgres password:

        ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
        azure/scripts/gen-api-credential.sh
        ```

        Keep the printed **password** for the connect step; the **hash** and
        **db-password** go to the apply. Reuse the **same** db-password on every
        re-apply.
      </Step>

      <Step title="Apply">
        PostgreSQL spin-up is the long pole; `--watch` streams progress.

        ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
        formae apply --mode reconcile azure/bootstrap.pkl --access public \
          --location <location> \
          --subscription-id <sub-id> --tenant-id <tenant-id> \
          --client-id <sp-appId> --client-secret '<sp-password>' \
          --api-user formae --api-password-hash '<hash>' --db-password '<dbpass>' \
          --ssh-public-key "$(cat ~/.ssh/id_ed25519.pub)" --watch
        ```

        To restrict who can reach the agent, add `--allowed-cidr <cidr>` (open by
        default). The agent is reachable at
        `<name>.<location>.cloudapp.azure.com:49684`.
      </Step>

      <Step title="Connect">
        The self-signed cert needs `-k` for curl; the profile script sets
        `insecureSkipVerify` for the CLI:

        ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
        curl -k https://formae-bootstrap.<location>.cloudapp.azure.com:49684/api/v1/health
        azure/scripts/write-bootstrap-profile.sh --profile bootstrap --access public \
          --fqdn formae-bootstrap.<location>.cloudapp.azure.com --user formae --password '<password>'
        formae status agent --profile bootstrap
        ```

        That writes a [profile](/documentation/guides/manage-profiles) carrying
        the agent's URL so your CLI can reach it.
      </Step>
    </Steps>
  </Tab>

  <Tab title="App Gateway">
    You also need a domain you own, a globally-unique Key Vault name, and the
    applying principal's objectId. For a browser-trusted cert, supply a PFX; omit it
    for a self-signed cert.

    <Steps>
      <Step title="Generate the API credential">
        ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
        azure/scripts/gen-api-credential.sh
        ```

        Keep the printed **password** for the connect step; the **hash** and
        **db-password** go to the apply.
      </Step>

      <Step title="Apply">
        ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
        formae apply --mode reconcile azure/bootstrap.pkl --access appgw \
          --location <location> \
          --subscription-id <sub-id> --tenant-id <tenant-id> \
          --client-id <sp-appId> --client-secret '<sp-password>' \
          --domain agent.example.com --kv-name <unique-kv-name> \
          --applier-object-id "$(az ad sp show --id <sp-appId> --query id -o tsv)" \
          --api-user formae --api-password-hash '<hash>' --db-password '<dbpass>' \
          --ssh-public-key "$(cat ~/.ssh/id_ed25519.pub)" --watch
        ```

        For a browser-trusted cert, add
        `--cert-pfx "$(base64 -i cert.pfx)" --cert-password '<pw>'`.
      </Step>

      <Step title="Point DNS at the gateway">
        Point `agent.example.com`'s DNS at the gateway's public IP
        (`formae-bootstrap-gw.<location>.cloudapp.azure.com`), then verify
        (self-signed: `-k`; trusted PFX: drop `-k`):

        ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
        curl -k https://agent.example.com/api/v1/health
        ```
      </Step>

      <Step title="Connect">
        The profile targets port `443` in appgw mode:

        ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
        azure/scripts/write-bootstrap-profile.sh --profile bootstrap --access appgw \
          --fqdn agent.example.com --user formae --password '<password>'
        formae status agent --profile bootstrap
        ```

        That writes a [profile](/documentation/guides/manage-profiles) carrying
        the agent's URL so your CLI can reach it.
      </Step>
    </Steps>

    <Note>
      Key Vault role assignments take a minute or two to propagate. If the first apply
      fails with a 403 on certificate creation, re-run the same command; it succeeds
      once the "Certificates Officer" grant lands.
    </Note>
  </Tab>

  <Tab title="Tailscale (tailnet)">
    You also need a Tailscale tailnet with a `tag:formae` tag, HTTPS certificates
    enabled, and a reusable auth key carrying that tag:

    * Sign in at [login.tailscale.com/start](https://login.tailscale.com/start)
      with an identity provider (the free Personal plan is enough) and note your
      tailnet name (e.g. `tailXXXX.ts.net`).
    * **DNS tab:** enable **MagicDNS** and **HTTPS Certificates** (required for the
      `*.ts.net` certificate).
    * **Access controls → Tags:** create a tag named `formae` (no `tag:` prefix),
      owned by yourself.
    * **Settings → Keys → Generate auth key:** **Reusable** on, **Ephemeral** off,
      **Tags** → `tag:formae`. Copy the `tskey-auth-...` value.

    <Steps>
      <Step title="Generate the API credential">
        ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
        azure/scripts/gen-api-credential.sh
        ```

        Keep the printed **password** for the connect step; the **hash** and
        **db-password** go to the apply.
      </Step>

      <Step title="Apply">
        ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
        formae apply --mode reconcile azure/bootstrap.pkl --access tailnet \
          --location <location> \
          --subscription-id <sub-id> --tenant-id <tenant-id> \
          --client-id <sp-appId> --client-secret '<sp-password>' \
          --ts-authkey '<tskey-auth-...>' --ts-hostname formae-bootstrap \
          --api-user formae --api-password-hash '<hash>' --db-password '<dbpass>' \
          --ssh-public-key "$(cat ~/.ssh/id_ed25519.pub)" --watch
        ```

        The agent joins your tailnet as `<ts-hostname>.<your-tailnet>.ts.net` and
        serves the API over a trusted certificate on port `49684`. Confirm it joined
        in the Tailscale admin console under **Machines**.
      </Step>

      <Step title="Connect">
        Your CLI machine must be on the same tailnet (use `tailscale switch` /
        `tailscale login` to reach it):

        ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
        azure/scripts/write-bootstrap-profile.sh --profile bootstrap --access tailnet \
          --fqdn formae-bootstrap.<your-tailnet>.ts.net --user formae --password '<password>'
        formae status agent --profile bootstrap
        ```

        That writes a [profile](/documentation/guides/manage-profiles) carrying
        the agent's URL so your CLI can reach it.
      </Step>
    </Steps>
  </Tab>
</Tabs>

## Operate the agent

Day-2 procedures for a bootstrapped agent.

### Update

Upgrade the agent the same way you created it: re-apply `bootstrap.pkl` with a
newer image. `--formae-image` is the version knob; find a target tag on
[GitHub Releases](https://github.com/platform-engineering-labs/formae/releases).

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
formae apply --mode reconcile azure/bootstrap.pkl <same flags as your apply> \
  --formae-image ghcr.io/platform-engineering-labs/formae:<new-version> \
  --watch
```

Pass the **same flags you applied with**, and critically the **same `--db-password`**
(it is set once at DB creation and carried in the agent config), so the reconcile
changes only the image. Then match your local CLI to the new version:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
formae update <new-version>
```

To roll back, re-apply with the previous image tag.

<Warning>
  **Keep the install you bootstrapped from.** The local install you ran
  `bootstrap.pkl` from holds the agent's own infrastructure (the resource group,
  PostgreSQL database, and VM that *are* the agent) in its state. That state lives
  only there, so re-applying `bootstrap.pkl` to upgrade or change the agent depends on
  keeping that install and its datastore.
</Warning>

### Add extra plugins

The bootstrap image ships the standard plugin set (AWS, Azure, GCP, OCI, OVH, and
`auth-basic`). To manage anything else, build a derived image and pass it as
`--formae-image`. See
[Extend the agent image](/documentation/guides/extend-the-agent-image).

### Tune for production

The bootstrap installer favors a lean default. For production, choose a larger
`--size` as your inventory grows (see the sizing table below), scope `public` mode
ingress with `--allowed-cidr`, and prefer `appgw` with your own PFX (or `tailnet`)
for a trusted certificate.

### Tear down

Destroy the stack, then deregister the target:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
formae destroy --query "stack:formae-bootstrap-azure"
formae apply --mode destroy azure/destroy-target.pkl
```

<Note>
  In `tailnet` mode, the agent's Tailscale node identity lives on the VM's OS disk and
  does not survive teardown. After destroying, delete the stale node manually in the
  Tailscale admin console.
</Note>

## Reference

<Accordion title="Bootstrap flags and sizing">
  Flags for `formae apply ... azure/bootstrap.pkl`:

  | Flag                                              | Mode    | Notes                                                                                                                         |
  | ------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
  | `--access`                                        | all     | `public` (default), `appgw`, or `tailnet`                                                                                     |
  | `--location`                                      | all     | Azure region; default `eastus`                                                                                                |
  | `--name`                                          | all     | resource name prefix (default `formae-bootstrap`); **must be globally unique**; drives the Postgres FQDN and public DNS label |
  | `--size`                                          | all     | t-shirt size (see below); default `small`                                                                                     |
  | `--subscription-id`                               | all     | target subscription (required)                                                                                                |
  | `--tenant-id` / `--client-id` / `--client-secret` | all     | remote agent's service principal (required)                                                                                   |
  | `--api-user` / `--api-password-hash`              | all     | basic-auth username (default `formae`) and bcrypt hash                                                                        |
  | `--db-password`                                   | all     | stable Postgres admin password; reuse on every re-apply                                                                       |
  | `--ssh-public-key`                                | all     | admin key on the VM (no inbound SSH is opened)                                                                                |
  | `--vnet-cidr` / `--subnet-cidr`                   | all     | address space; default `10.100.0.0/16` / `10.100.1.0/24`                                                                      |
  | `--formae-image`                                  | all     | agent image reference, the version knob                                                                                       |
  | `--allowed-cidr`                                  | public  | source CIDR allowed to the API (open by default)                                                                              |
  | `--domain`                                        | appgw   | hostname on the cert; point its DNS at the gateway IP                                                                         |
  | `--kv-name`                                       | appgw   | globally-unique Key Vault name (3 to 24 chars) the stack creates                                                              |
  | `--applier-object-id`                             | appgw   | objectId of the applying principal (granted Certificates Officer)                                                             |
  | `--cert-pfx` / `--cert-password`                  | appgw   | optional base64 PFX + password for a trusted cert (else self-signed)                                                          |
  | `--ts-authkey`                                    | tailnet | reusable Tailscale auth key (`tag:formae`)                                                                                    |
  | `--ts-hostname`                                   | tailnet | tailnet (MagicDNS) hostname (defaults to `--name`)                                                                            |

  Passing a flag that belongs to another mode is ignored or fails fast with a clear
  message.

  `--size` maps to an Azure VM size. Memory has a \~2 GB floor regardless of inventory
  size, because the agent image loads all its bundled resource plugins at once. Sizes
  use the Dsv6 family, which has default vCPU quota on fresh subscriptions.

  | size     | VM size           | vCPU / memory | rough capacity          |
  | -------- | ----------------- | ------------- | ----------------------- |
  | `small`  | `Standard_D2s_v6` | 2 / 8 GB      | up to \~1,000 resources |
  | `medium` | `Standard_D2s_v6` | 2 / 8 GB      | \~1,000 to 5,000        |
  | `large`  | `Standard_D4s_v6` | 4 / 16 GB     | \~5,000 to 10,000       |
  | `xlarge` | `Standard_D8s_v6` | 8 / 32 GB     | \~10,000 to 20,000      |
</Accordion>

## See also

* [Manage profiles](/documentation/guides/manage-profiles): connect the CLI to the agent you just started, and switch between environments.
* [Configuration](/documentation/reference/configuration): agent and CLI settings, including authentication.
* [Architecture](/documentation/concepts/architecture): how the client, agent, and plugins fit together.
