Provisioning VMs
Production-shape multi-service VM environments; private subnet with NAT, optional cloud LB, IAM-gated exec, no SSH.
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)
| 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 NGINX routing between Docker containers | VM (the default cloud-init configures this) |
| Lightweight environment for testing; single host, single port | VM |
| Burst traffic with horizontal scaling | VM 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:
| Resource | Notes |
|---|---|
| VM(s) | In a private subnet of the chosen VPC. No public IP. |
| VM security group / firewall | Inbound: only from within the VPC + LB security group. No public SSH; port 22 is not publicly reachable. |
| Launch template / instance template | Stores 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 VM | Includes the exec channel role (SSM / OS Config); see below |
| Tag set | zop: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:
stepValidate → stepCreateNetwork → stepCreateIAM → stepCreateLaunchTemplate → stepCreateVMs → [LB sub-steps when LB attached] → stepRegisterSpace → stepFinalizeFor 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:
- Install Docker
- Create an
nginx-on-zop-netDocker bridge network - Pre-install NGINX as a Docker container on the bridge for multi-service routing on a single VM
- 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:
| Cloud | Channel | Command |
|---|---|---|
| AWS | SSM Session Manager | aws ssm start-session --target i-xxxx |
| GCP | IAP tunnel | gcloud compute os-config-instance-os-policy-assignment-reports describe (for managed inventory) or gcloud compute ssh --tunnel-through-iap for interactive |
| Azure | Azure 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:
| Cloud | LB type | TLS termination |
|---|---|---|
| AWS | Application Load Balancer | At the LB via ACM certificate |
| GCP | HTTPS Load Balancer | At the LB via Google-managed certificate |
| Azure | Application Gateway | At 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:
PATCH /provisioning-jobs/{id}{ "vmOp": "attach_lb" }…or:
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:
| 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 NGINX bootstrap completed? |
No new tables. Adding a new field to track is a struct extension, not a migration.
Updating 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; 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:
- 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 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 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. |