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.
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:createpolicy. 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)
| Situation | Pick |
|---|---|
| Multiple services, container-native, need orchestration | Cluster (EKS / GKE / AKS); see Clusters |
| Legacy workload that runs as a process, not a container | VM |
| Multi-service host with Caddy routing between Docker containers | VM (Docker and Caddy install automatically when the VM is provisioned) |
| Lightweight environment for testing; single host, single port | VM |
| Burst traffic with horizontal scaling | VM 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:
create_network → create_iam → LB security group → VM security group → create VM → wait for ready → bootstrap → create load balancer → attach VMs to LB → register_spaceThe 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:
- Install Docker
- Create a Docker bridge network for the services on the VM
- 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:
| Cloud | Channel | Command |
|---|---|---|
| AWS | SSM Session Manager | aws ssm start-session --target i-xxxx |
| GCP | IAP tunnel | gcloud compute ssh --tunnel-through-iap |
| Azure | Azure 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
Open the drawer
Go to Infrastructure → Provision and choose Virtual Machine (EC2, Compute Engine, or Azure VM).
Account and region
Pick the Cloud Account and the Region.
Name and size
Enter a name and pick an Instance size.
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.
Provision
Check the step estimate and click Provision. Follow the job in Activity → Provisioning Jobs.
What a VM job provisions
| Resource | Notes |
|---|---|
| VM(s) | In a private subnet of the chosen VPC. No public IP. |
| VM security group / firewall | Inbound: 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 template | Stores the AMI / image, instance type, IAM role, cloud-init userdata, and tags |
| Cloud LB | Always created. It is the only public route to the VMs and passes TCP through to them. |
| IAM role for the VM | Includes what the IAM-gated exec channel needs. |
| Tag set | zop: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:
PATCH /provisioning-jobs/{id}{ "vmOp": "attach_lb" }…or:
PATCH /provisioning-jobs/{id}{ "vmOp": "detach_lb" }There is no separate route. The PATCH route handles VM ops alongside other partial updates.
Update VMs
| Change | What happens |
|---|---|
min/max/desired in ASG mode | Patched on the ASG. Existing instances stay; new ones come from the launch template. |
| Cloud-init userdata change | New 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 change | Same as userdata: new launch template version, manual rotation. |
| Security group change | Applied 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.
| Change | Downtime |
|---|---|
| Machine type (with or without a disk change) | Restarts a running VM |
| Boot disk only, AWS | None |
| Boot disk only, GCP or Azure | Restarts; 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:
- Detaches the LB (if attached)
- Terminates the VMs (Fixed mode) or sets ASG/MIG
min=max=desired=0then deletes (ASG mode) - Deletes the launch template
- Deletes the security group
- Tears down the IAM role
- 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:
| Field | What it carries |
|---|---|
VMs[] | Per-instance IDs, IPs, lifecycle state |
VMSecurityGroupID | The security group / firewall rule attached to the VMs |
LoadBalancerID | The LB if attached |
ScalingGroupID | ASG / MIG ID in ASG mode |
LaunchTemplateID | The current launch template version |
ExistingResourceGroup | (Azure) the resource group the VMs landed in; preserved if pre-existing |
BootstrapNginxReady | Has 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 scope | Why |
|---|---|
| Configuration management beyond cloud-init bootstrap | Use Ansible / Chef / SaltStack / Puppet on top of ZopDay’s VMs. ZopDay only sets up the Docker baseline. |
| Patch management | Use AWS Systems Manager Patch Manager / OS Config / Update Management. These tie into the exec channel ZopDay enables. |
| SSH keys | None 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.