Skip to main content Skip to content

Provisioning VMs

Provision a production-shaped VM environment (private subnet, NAT egress, a cloud load balancer, IAM-gated exec, no public SSH), then resize, update, or tear it down.

10 min read

Provision VMs when Kubernetes is the wrong abstraction: multi-service VMs running Docker, legacy workloads that aren’t containerised, or a single shared host with Caddy routing between services. The default shape is production-ready: private subnet, NAT egress, a cloud load balancer, no public SSH, and IAM-gated exec.

Before you start

  • A cloud account connected with the provisioning permissions (on AWS this includes EC2 and Auto Scaling). See Cloud permissions.
  • The provisioning:create policy. See RBAC.
  • A pool mode: Fixed or ASG. On Azure, only Fixed is supported today.
  • A networking decision: a new VPC or an existing one. See Networking.

How it works

When to provision VMs (vs. clusters)

SituationPick
Multiple services, container-native, need orchestrationCluster (EKS / GKE / AKS); see Clusters
Legacy workload that runs as a process, not a containerVM
Multi-service host with Caddy routing between Docker containersVM (Docker and Caddy install automatically when the VM is provisioned)
Lightweight environment for testing; single host, single portVM
Burst traffic with horizontal scalingVM in ASG / MIG mode

A VM is not a “lite cluster”. It is a different abstraction for different workloads. Besides provisioning the VM as infrastructure, ZopDay’s deployer can ship containers straight to it; see Deploy to a VM.

Pool modes

Fixed

N specific VMs via direct RunInstances (AWS) or compute.instances.insert (GCP). Each VM has a stable identity. Best for stateful workloads or specific-IP requirements.

ASG (Auto Scaling)

AWS Auto Scaling Group or GCP Managed Instance Group. Min / max / desired capacity and scaling policies, with the instance lifecycle managed by the cloud autoscaler. Best for stateless web servers.

Azure scale sets (VMSS) are not supported yet: an ASG-mode job on Azure is rejected with a clean Invalid response.

Pipeline steps

The VM pipeline shares its network, IAM, and space registration steps with the cluster pipeline. Every VM job runs the same 10 steps:

Terminal window
create_network → create_iam → LB security group → VM security group
→ create VM → wait for ready → bootstrap
→ create load balancer → attach VMs to LB → register_space

The load balancer is mandatory, so there is no shorter variant without one. The Provision drawer shows the same sequence with a time estimate (about 6 to 8 minutes) before you start.

Cloud-init bootstrap

Every VM ZopDay provisions runs the same cloud-init userdata across clouds:

  1. Install Docker
  2. Create a Docker bridge network for the services on the VM
  3. Write a sentinel file once everything is wired

The job waits until the bootstrap has finished before it attaches the VM to the load balancer. Caddy, which routes between services and terminates TLS, installs as a VM component when the VM is provisioned, along with Docker, so the VM is healthy before your first deploy.

Load balancer

Every VM job creates a cloud load balancer. It is a layer 4 (TCP passthrough) load balancer: it forwards connections to the VMs on the service ports without terminating them, which preserves the client’s source IP. TLS terminates on the VM, at Caddy, with Let’s Encrypt certificates. Caddy’s baseline config answers health checks on port 80 from the moment it installs, so the load balancer reports the VMs healthy before the first service deploys.

Exec channel: no public SSH

ZopDay never provisions public SSH access. Port 22 is not publicly reachable, the VM has no public IP, and ZopDay does not put your public key in authorized_keys. Interactive access is only via the cloud’s IAM-gated exec channel:

CloudChannelCommand
AWSSSM Session Manageraws ssm start-session --target i-xxxx
GCPIAP tunnelgcloud compute ssh --tunnel-through-iap
AzureAzure Run Command (preview)az vm run-command invoke

Exec is IAM-gated end to end. Your AWS/GCP/Azure identity decides whether you can start a session at all. With no public SSH and no port 22 exposed to the internet, there is no public brute-force surface. Deploys to the VM use the same channels, dispatched from the GitHub Actions runner.

Provision a VM environment

  1. Open the drawer

    Go to Infrastructure → Provision and choose Virtual Machine (EC2, Compute Engine, or Azure VM).

  2. Account and region

    Pick the Cloud Account and the Region.

  3. Name and size

    Enter a name and pick an Instance size.

  4. Network

    Choose Create new VPC (recommended: subnets, NAT, IGW, security groups) or Use existing VPC. Advanced settings holds further VM and network options, with defaults already applied.

  5. Provision

    Check the step estimate and click Provision. Follow the job in Activity → Provisioning Jobs.

What a VM job provisions

ResourceNotes
VM(s)In a private subnet of the chosen VPC. No public IP.
VM security group / firewallInbound: the service ports only. The load balancer passes TCP straight through and preserves the client’s source IP, so these ports are open to 0.0.0.0/0; everything else stays closed. No public SSH; port 22 is not publicly reachable.
Launch template / instance templateStores the AMI / image, instance type, IAM role, cloud-init userdata, and tags
Cloud LBAlways created. It is the only public route to the VMs and passes TCP through to them.
IAM role for the VMIncludes what the IAM-gated exec channel needs.
Tag setzop:orgId, zop:jobId, zop:role, zop:managedBy on every resource

Attach or detach the load balancer

After the VM job lands, you can detach and reattach the LB without re-provisioning the VMs. Use the existing PATCH route with the typed vmOp discriminator:

Terminal window
PATCH /provisioning-jobs/{id}
{ "vmOp": "attach_lb" }

…or:

Terminal window
PATCH /provisioning-jobs/{id}
{ "vmOp": "detach_lb" }

There is no separate route. The PATCH route handles VM ops alongside other partial updates.

Update VMs

ChangeWhat happens
min/max/desired in ASG modePatched on the ASG. Existing instances stay; new ones come from the launch template.
Cloud-init userdata changeNew launch template version. Existing instances do not auto-rotate: terminate them explicitly to get the new userdata, or trigger a rolling instance refresh.
AMI / image changeSame as userdata: new launch template version, manual rotation.
Security group changeApplied immediately to all instances (security groups are mutable in place on AWS / GCP).

Rolling instance refresh on ASG: aws autoscaling start-instance-refresh (AWS) or recreating the MIG (GCP). ZopDay does not run rolling refresh automatically, because the right approach depends on your workload.

Resizing a VM in place

You can change a VM’s machine type and grow its boot disk without re-provisioning it. The resize runs as an ordinary update job, the same kind clusters and datastores use. It works on VMs ZopDay discovered as well as VMs ZopDay created.

The change happens in place; the VM is not replaced. The instance ID and private IP survive on all three clouds, so your space config, load-balancer registration, and attached domains stay correct with nothing to update afterwards.

ChangeDowntime
Machine type (with or without a disk change)Restarts a running VM
Boot disk only, AWSNone
Boot disk only, GCP or AzureRestarts; GCP grows the filesystem at boot, and Azure cannot resize an allocated disk

Before you run a resize, note the following:

  • Disks grow only. No cloud allows shrinking one.
  • A stopped VM is resized and left stopped. ZopDay never starts it for you, so a resize can’t boot a machine that you or an on/off schedule turned off and put it back on the bill. Its filesystem catches up on the next boot.
  • One VM at a time. A multi-VM pool is resized VM by VM, so the load balancer keeps serving from the rest while each one cycles.

Tear down a VM environment

DELETE /provisioning-jobs/{id} on a VM job starts a deletion job that:

  1. Detaches the LB (if attached)
  2. Terminates the VMs (Fixed mode) or sets ASG/MIG min=max=desired=0 then deletes (ASG mode)
  3. Deletes the launch template
  4. Deletes the security group
  5. Tears down the IAM role
  6. Tears down networking (if ZopDay created the VPC; existing VPCs are not touched)

VM terminations typically take 1–2 minutes. ASG/MIG deletion can take 5–10 minutes depending on instance count.

Reference

VM lifecycle state

ZopDay tracks per-VM lifecycle state inside the job’s config JSON. Fields include:

FieldWhat it carries
VMs[]Per-instance IDs, IPs, lifecycle state
VMSecurityGroupIDThe security group / firewall rule attached to the VMs
LoadBalancerIDThe LB if attached
ScalingGroupIDASG / MIG ID in ASG mode
LaunchTemplateIDThe current launch template version
ExistingResourceGroup(Azure) the resource group the VMs landed in; preserved if pre-existing
BootstrapNginxReadyHas the cloud-init bootstrap completed?

Cost notes

VM cost = instance hourly × count × hours + EBS / persistent disk storage + LB hourly (if attached) + NAT data processing.

For dev/staging environments, attach a ZopNight schedule to the VM pool to stop instances on weeknights and weekends. The savings on a 10-instance dev pool with weekday-only hours are typically 60–70% vs 24×7.

What ZopDay does NOT do on VMs

Out of scopeWhy
Configuration management beyond cloud-init bootstrapUse Ansible / Chef / SaltStack / Puppet on top of ZopDay’s VMs. ZopDay only sets up the Docker baseline.
Patch managementUse AWS Systems Manager Patch Manager / OS Config / Update Management. These tie into the exec channel ZopDay enables.
SSH keysNone ever provisioned. Use the exec channel.

Troubleshooting

The load balancer reports a VM unhealthy

Check that Caddy is installed on the VM and that the service ports on the load balancer and the VM security group match. Caddy answers health checks on port 80 as soon as it installs.

An ASG-mode job on Azure was rejected

Azure scale sets (VMSS) aren’t supported yet, so ASG mode on Azure returns Invalid. Use Fixed mode on Azure.

Existing instances didn't pick up new userdata or a new image

A userdata or image change creates a new launch template version; existing instances don’t rotate on their own. Terminate them, or run a rolling instance refresh (aws autoscaling start-instance-refresh on AWS, or recreate the MIG on GCP).

A resized VM is still stopped

That is by design. ZopDay resizes a stopped VM and leaves it stopped, so it never boots a machine a schedule turned off. Start it yourself when you need it.

Next steps

Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001· 20–60% off the bill, first month· 4 platforms · 1 console· Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001· 20–60% off the bill, first month· 4 platforms · 1 console·