VM backups

Development preview. This feature is in preview. Please send feedback to support@exe.dev.

backup copies a VM's disk to an S3 bucket configured via a Workload Identity Federation integration. Backups are desync chunk stores, and can be restored both onto an exe.dev VM as well as independently of exe.

Setting up AWS and Exe

The best way to configure this is to point your agent at these docs.

Create an S3 bucket:

export AWS_ACCOUNT_ID=123456789012
export BUCKET_NAME=acme-exe-backups
export ROLE_NAME=exe-dev-backups
export AWS_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/${ROLE_NAME}"
aws s3 mb "s3://${BUCKET_NAME}" --region us-west-2

Be aware that a chunk in this bucket being old does not imply that it is not part of a backup, so be wary of lifecycle rules that expire objects.

Create a workload identity integration for the role:

ssh exe.dev integrations add wif --name=backups --audience=sts.amazonaws.com \
  --consumer=aws --metadata=role_arn="$AWS_ROLE_ARN"

Set its Issuer and Subject in your shell:

export EXE_WIF_ISSUER=https://exe.dev/issuer/example
export EXE_WIF_SUBJECT=sub-ABCDEFGHIJKLMNOPQRSTUVWXYZ
export AWS_OIDC_CONDITION_PREFIX="${EXE_WIF_ISSUER#https://}"
export AWS_OIDC_PROVIDER_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/${AWS_OIDC_CONDITION_PREFIX}"

Add the backup audience to the issuer's IAM OIDC provider:

aws iam add-client-id-to-open-id-connect-provider \
  --open-id-connect-provider-arn "$AWS_OIDC_PROVIDER_ARN" --client-id exe.dev/vm-backup

If you have no provider for the issuer yet, create it instead:

aws iam create-open-id-connect-provider \
  --url "$EXE_WIF_ISSUER" --client-id-list exe.dev/vm-backup

Create roles for backups:

cat > /tmp/exe-backups-trust.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {"Federated": "${AWS_OIDC_PROVIDER_ARN}"},
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "${AWS_OIDC_CONDITION_PREFIX}:aud": "exe.dev/vm-backup",
          "${AWS_OIDC_CONDITION_PREFIX}:sub": "${EXE_WIF_SUBJECT}"
        }
      }
    }
  ]
}
EOF

aws iam create-role --role-name "$ROLE_NAME" --max-session-duration 43200 \
  --assume-role-policy-document file:///tmp/exe-backups-trust.json

Grant it the bucket:

cat > /tmp/exe-backups-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::${BUCKET_NAME}",
      "Condition": {
        "StringLike": {
          "s3:prefix": [
            "chunks/*",
            "indexes/*"
          ]
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": [
        "arn:aws:s3:::${BUCKET_NAME}/chunks/*",
        "arn:aws:s3:::${BUCKET_NAME}/indexes/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": [
        "arn:aws:s3:::${BUCKET_NAME}/chunks/*",
        "arn:aws:s3:::${BUCKET_NAME}/indexes/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "s3:DeleteObject",
      "Resource": [
        "arn:aws:s3:::${BUCKET_NAME}/chunks/*",
        "arn:aws:s3:::${BUCKET_NAME}/indexes/*"
      ]
    }
  ]
}
EOF

aws iam put-role-policy --role-name "$ROLE_NAME" --policy-name exe-dev-backups \
  --policy-document file:///tmp/exe-backups-policy.json

backup new needs only the ListBucket on chunks/* and the PutObject; backup restore only the GetObject. To keep backups under a prefix, pass --bucket=bucket/prefix and put the prefix in front of chunks/ and indexes/ in the policy.

Back up and restore

backup new --bucket=acme-exe-backups --integration=backups web
backup list
backup status web-20261008-1910
backup watch web-20261008-1910
backup restore web-20261008-1910 web-restored

A backup syncs the VM's filesystem and snapshots its disk. With --freeze, writes in the VM also pause briefly while the snapshot is taken.

Delete old backups

backup vacuum deletes the backups older than --keep-days (at least 7), except each VM's newest, and then the chunks no remaining backup uses. Only team admins can vacuum with a team's integration.

backup vacuum --bucket=acme-exe-backups --integration=backups --keep-days=30

Restore without exe.dev

A backup is a disk image holding one ext4 filesystem, with no partition table. With AWS credentials that can read the bucket in S3_ACCESS_KEY, S3_SECRET_KEY and S3_SESSION_TOKEN:

export STORE=s3+https://s3.us-west-2.amazonaws.com/${BUCKET_NAME}
desync extract -s "$STORE/chunks/" "$STORE/indexes/web-20261008-1910.caibx" disk.img
sudo mount -o loop,ro,noload disk.img /mnt