# How to configure a VM with cloud-init

Source: https://docs.quake.ai/docs/compute/how-to/configure-vm-cloud-init
Markdown: https://docs.quake.ai/docs/compute/how-to/configure-vm-cloud-init.md

---

# How to configure a VM with cloud-init

Write cloud-init user data, pass it when you launch an instance, and confirm the guest applied your baseline on first boot.



cloud-init is standard upstream behavior on cloud images because Quake AI runs OpenStack/Nova. Quake AI does not ship or support a separate first-boot service. You own the user-data content and any software cloud-init installs.



For background on formats and the one-shot boot model, see [cloud-init and first-boot configuration](/docs/compute/concepts/cloud-init). For a full production launch workflow, see [How to provision a production-ready VM](/docs/compute/how-to/provision-production-vm).

<PrerequisiteBlock methods={["console", "cli", "api", "terraform"]}>

- An [SSH key pair](/docs/tools/add-ssh-key) uploaded to your project
- A [network and subnet](/docs/network/how-to/create-network) for the instance
- A security group that allows SSH (TCP 22) from your admin range

</PrerequisiteBlock>

## Step 1. Write a minimal cloud-config file

Create `cloud-init.yaml` with a non-root sudo user, package updates, and a hostname:

```yaml
#cloud-config
hostname: APP_HOSTNAME
users:
  - name: deploy
    groups: sudo
    shell: /bin/bash
    sudo: ALL=(ALL) NOPASSWD:ALL
    ssh_authorized_keys:
      - YOUR_SSH_PUBLIC_KEY
package_update: true
package_upgrade: true
```

Replace `APP_HOSTNAME` and `YOUR_SSH_PUBLIC_KEY`. The `#cloud-config` header tells cloud-init to parse the file as cloud-config YAML, not a shell script.

## Step 2. Use common cloud-config modules

Add modules as your baseline grows. Each example is self-contained; merge the keys you need into one `#cloud-config` file.

**`users`** (shown above): creates login accounts and injects SSH public keys.

**`packages`:**

```yaml
packages:
  - nginx
  - fail2ban
```

**`package_update` / `package_upgrade`:** refresh package indexes and apply upgrades on first boot (shown in Step 1).

**`write_files`:**

```yaml
write_files:
  - path: /etc/motd
    content: |
      Managed by cloud-init baseline
    owner: root:root
    permissions: "0644"
```

**`bootcmd` vs `runcmd`:** `bootcmd` runs early every boot (rarely needed on first-boot baselines). `runcmd` runs late on first boot only:

```yaml
runcmd:
  - systemctl enable nginx
  - systemctl start nginx
```

## Step 3. Pass user data at launch

<MethodTabs>
<Method label="Console">

1. Select **Compute** > **Instances** > **Create Instance**.
2. Complete **Base Config** and **Network Config**.
3. On **System Config**, open **Advanced Options**.
4. Paste your cloud-config YAML into **User Data**.
5. Finish **Confirm Config** and launch the instance.

</Method>
<Method label="CLI">

Create a security group that opens SSH from your admin CIDR if you do not already have one:

```bash
openstack security group create cloudinit-ssh
openstack security group rule create \
  --protocol tcp --dst-port 22 --remote-ip YOUR_ADMIN_CIDR/32 \
  cloudinit-ssh
```

Replace `YOUR_ADMIN_CIDR` with the public IP you SSH from. See [create a security group](/docs/network/how-to/create-security-group) for rule options.

Pass your cloud-config file with `--user-data`. All Quake AI flavors have `disk=0`, so include `--boot-from-volume` with the root volume size in GiB:

```bash
openstack server create \
  --image Ubuntu-24.04 \
  --flavor s1a.medium \
  --boot-from-volume 10 \
  --network YOUR_NETWORK \
  --key-name YOUR_KEYPAIR \
  --security-group default \
  --security-group cloudinit-ssh \
  --user-data cloud-init.yaml \
  APP_INSTANCE_NAME
```

Nova accepts plain text; the CLI uploads the file contents.

</Method>
<Method label="API">

<ComputeApiEnvironment />

Include a base64-encoded `user_data` field on the server create request:

```bash
USER_DATA_B64=$(base64 -w0 cloud-init.yaml)

curl -X POST "$OS_COMPUTE_URL/servers" \
  -H "X-Auth-Token: $OS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"server\": {
      \"name\": \"APP_INSTANCE_NAME\",
      \"flavorRef\": \"FLAVOR_ID\",
      \"networks\": [{\"uuid\": \"NETWORK_ID\"}],
      \"key_name\": \"YOUR_KEYPAIR\",
      \"security_groups\": [{\"name\": \"default\"}, {\"name\": \"cloudinit-ssh\"}],
      \"user_data\": \"${USER_DATA_B64}\",
      \"block_device_mapping_v2\": [{
        \"boot_index\": 0,
        \"uuid\": \"IMAGE_ID\",
        \"source_type\": \"image\",
        \"destination_type\": \"volume\",
        \"volume_size\": 10,
        \"delete_on_termination\": true
      }]
    }
  }"
```

On macOS, use `base64 cloud-init.yaml | tr -d '\n'` instead of `base64 -w0`.

</Method>
<Method label="Terraform">

Reference the file from OpenTofu:

```hcl
resource "openstack_compute_instance_v2" "app" {
  name            = "APP_INSTANCE_NAME"
  flavor_name     = "s1a.medium"
  image_name      = "Ubuntu-24.04"
  key_pair        = "YOUR_KEYPAIR"
  security_groups = ["default", "cloudinit-ssh"]

  user_data = file("${path.module}/cloud-init.yaml")

  network {
    name = "YOUR_NETWORK"
  }

  block_device {
    source_type           = "image"
    destination_type      = "volume"
    volume_size           = 10
    boot_index            = 0
    delete_on_termination = true
  }
}
```

Heat stacks use the `user_data` property on `OS::Nova::Server` with the same payload. See [Heat simple stack](/resources/iac-templates/heat-simple-stack) for a HOT example.

</Method>
</MethodTabs>

## Step 4. Debug first-boot failures

When SSH fails or packages are missing, inspect cloud-init before you rebuild the instance.

SSH in (or open the [virtual machine console](/docs/compute/how-to/use-vm-console) if SSH is not up yet) and run:

```bash
cloud-init status --wait
sudo tail -n 100 /var/log/cloud-init.log
sudo tail -n 100 /var/log/cloud-init-output.log
```

`status --wait` blocks until cloud-init finishes or reports an error. The log files show module execution order and shell output from `runcmd`.

Validate cloud-config syntax locally:

```bash
cloud-init schema --config-file cloud-init.yaml
```

Fix YAML errors, then launch a **new** instance with corrected user data. cloud-init does not re-run the full first-boot sequence on an instance that already completed it.

## Step 5. Verify the baseline landed

After `cloud-init status` reports `done`:

```bash
hostname
id deploy
dpkg -l nginx 2>/dev/null || rpm -q nginx 2>/dev/null
```

Confirm the hostname, the `deploy` user, and any packages you declared are present. SSH as `deploy@INSTANCE_ADDRESS` with the matching private key.

## See also

- [cloud-init and first-boot configuration](/docs/compute/concepts/cloud-init)
- [How to provision a production-ready VM](/docs/compute/how-to/provision-production-vm)
- [Configure a VM with Ansible](/resources/iac-templates/ansible-configure-vm)
