Skip to main content Skip to content

Provisioning VMs

Production-shape multi-service VM environments; private subnet with NAT, optional cloud LB, IAM-gated exec, no SSH.

8 min read Last updated: 

ZopDay provisions VMs for the case where Kubernetes is the wrong abstraction; multi-service VMs running Docker, legacy workloads that aren’t containerised, or environments where you want a single shared host with NGINX routing between services. The default shape is production-ready: private subnet, NAT egress, no public SSH, IAM-gated exec.

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 NGINX routing between Docker containersVM (the default cloud-init configures this)
Lightweight environment for testing; single host, single portVM
Burst traffic with horizontal scalingVM in ASG / MIG mode

VMs are not a “lite cluster”; they’re a different abstraction for genuinely different workloads.

Pool modes

ZopDay supports two 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, scaling policies, instance lifecycle managed by the cloud autoscaler. Best for stateless web servers.

Azure VMSS provisioning is in preview / not yet GA; when provisioning a VM job with provider=Azure, the wizard surfaces a clean stub return (Invalid) and points you at when it will be available. The Azure exec channel is likewise gated on Azure GA.

What the wizard provisions

The default VM environment lands:

ResourceNotes
VM(s)In a private subnet of the chosen VPC. No public IP.
VM security group / firewallInbound: only from within the VPC + LB security group. No public SSH; port 22 is not publicly reachable.
Launch template / instance templateStores the AMI / image, instance type, IAM role, cloud-init userdata, tags
Cloud LB (optional)TLS-terminated. Cert source = CertSource{AWSACM, GCPManaged, AzureKeyVault}
IAM role for the VMIncludes the exec channel role (SSM / OS Config); see below
Tag setzop:orgId, zop:jobId, zop:role, zop:managedBy on every resource

Pipeline steps

The VM pipeline reuses shared steps with the cluster pipeline (stepCreateNetwork, stepCreateIAM, stepRegisterSpace) via j.ResourceType branching. The full sequence:

Terminal window
stepValidate → stepCreateNetwork → stepCreateIAM → stepCreateLaunchTemplate
→ stepCreateVMs → [LB sub-steps when LB attached]
→ stepRegisterSpace → stepFinalize

For a VM job without a load balancer: 7 steps total. For a VM job with a load balancer: 11 steps total (the LB sub-steps cover the cloud-side load balancer creation, target group / backend pool, listener / forwarding rule, and DNS record).

Cloud-init bootstrap

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

  1. Install Docker
  2. Create an nginx-on-zop-net Docker bridge network
  3. Pre-install NGINX as a Docker container on the bridge for multi-service routing on a single VM
  4. Write a sentinel file once everything is wired

The sentinel file is what ZopDay polls during stepCreateVMs to confirm the VM is actually ready to accept workloads. Until the sentinel exists, the VM is in bootstrap state and the job waits.

If you don’t want NGINX pre-installed, the wizard’s “Bare metal” mode skips that part of the cloud-init. You still get Docker and the network setup; NGINX is your problem.

This NGINX baseline is for VMs provisioned as standalone infrastructure; when a VM is used as a ZopDay deploy substrate, Caddy handles routing instead (see Deploy to a VM).

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 os-config-instance-os-policy-assignment-reports describe (for managed inventory) or gcloud compute ssh --tunnel-through-iap for interactive
AzureAzure Run Command (in preview / not yet GA)az vm run-command invoke

Exec is IAM-gated end-to-end; your AWS/GCP/Azure identity gates whether you can even start a session. Because there is no public SSH and no port 22 exposed to the internet, there is no public brute-force surface; access flows only through the cloud provider’s IAM-gated channel.

The exec channel for each cloud is set via the ExecMode field: SSM (AWS), OSConfig (GCP), AzureRunCmd (Azure).

Load balancer (optional)

When you toggle “with load balancer” in the wizard:

CloudLB typeTLS termination
AWSApplication Load BalancerAt the LB via ACM certificate
GCPHTTPS Load BalancerAt the LB via Google-managed certificate
AzureApplication GatewayAt the gateway via Key Vault certificate

Health checks default to GET /healthz on port 80; override in the wizard if your service uses a different path. If the LB reports the target unhealthy, a mismatched health path or port is the most common cause; correct it by overriding the check in the wizard (or on the load balancer after provisioning).

LB attach / detach after provisioning

LBs can be attached or detached after the VM job lands 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" }

No new route surface; the PATCH route handles VM ops alongside other partial updates.

VM lifecycle state

ZopDay tracks per-VM lifecycle state on InfraConfig 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 NGINX bootstrap completed?

No new tables. Adding a new field to track is a struct extension, not a migration.

Updating 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; that’s a workload-shape decision.

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 is typically 60–70% vs 24×7.

Teardown

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

  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 are typically 1–2 minutes. ASG/MIG deletion can take 5–10 minutes depending on instance count.

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.
Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001 · zero-trust· 30% average cloud cost cut· 4 platforms · 1 console· Multi-cloud automation· Production-ready in 30 min· SOC 2 · ISO 27001 · zero-trust· 30% average cloud cost cut· 4 platforms · 1 console·