AWS nested virtualization is here - what it means for Go developers
AWS just announced support for nested virtualization on EC2 instances. This flew under the radar for many, but it’s a big deal if you’re building infrastructure tools, CI/CD systems, or testing frameworks in Go.
The Hacker News discussion had plenty of chatter about use cases. Let’s look at what this means for Go developers specifically.
What is nested virtualization?
Nested virtualization lets you run a VM inside a VM. Your EC2 instance becomes a hypervisor that can spawn its own virtual machines.
Before this, AWS blocked it. You couldn’t run KVM, QEMU, or any hypervisor inside EC2. Now you can on certain instance types.
This matters for:
- Running Kubernetes clusters with minikube using a VM driver. kind is still useful for local Kubernetes, but it runs nodes as containers rather than nested VMs
- Testing infrastructure automation tools
- Building multi-tenant platforms
- CI/CD pipelines that need full VM isolation
Running VMs from Go
Go has solid libraries for VM management. Here’s how you might use libvirt bindings to create a VM programmatically:
package main
import (
"fmt"
"net"
"time"
"github.com/digitalocean/go-libvirt"
)
func main() {
// Connect to libvirt socket
c, err := net.DialTimeout("unix", "/var/run/libvirt/libvirt-sock", 2*time.Second)
if err != nil {
panic(err)
}
l := libvirt.New(c)
if err := l.Connect(); err != nil {
panic(err)
}
defer l.Disconnect()
// List running domains
domains, _, err := l.ConnectListAllDomains(1, libvirt.ConnectListDomainsActive)
if err != nil {
panic(err)
}
for _, d := range domains {
fmt.Printf("Running VM: %s\n", d.Name)
}
}
This is useful for building orchestration tools that manage VMs directly.
Building a VM health checker
Here’s a more practical example. Say you’re building a system that monitors nested VMs:
package main
import (
"context"
"fmt"
"net"
"time"
"github.com/digitalocean/go-libvirt"
)
type VMStatus struct {
Name string
State string
Memory uint64
CPUs uint32
}
func checkVMHealth(ctx context.Context, l *libvirt.Libvirt) ([]VMStatus, error) {
domains, _, err := l.ConnectListAllDomains(1, libvirt.ConnectListDomainsActive)
if err != nil {
return nil, fmt.Errorf("listing domains: %w", err)
}
var statuses []VMStatus
for _, d := range domains {
state, _, err := l.DomainGetState(d, 0)
if err != nil {
continue
}
info, err := l.DomainGetInfo(d)
if err != nil {
continue
}
statuses = append(statuses, VMStatus{
Name: d.Name,
State: stateToString(libvirt.DomainState(state)),
Memory: info.Memory,
CPUs: uint32(info.NrVirtCpu),
})
}
return statuses, nil
}
func stateToString(state libvirt.DomainState) string {
switch state {
case libvirt.DomainRunning:
return "running"
case libvirt.DomainPaused:
return "paused"
case libvirt.DomainShutdown:
return "shutting down"
default:
return "unknown"
}
}
Notice the use of context for cancellation support. If you’re not familiar with context patterns, check out what is context in Go.
Testing with nested VMs
The real power comes in testing. You can spin up isolated VMs for integration tests:
func TestDatabaseMigration(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
vm, err := createTestVM(ctx, "postgres-test")
if err != nil {
t.Fatalf("creating test VM: %v", err)
}
defer vm.Destroy()
// Wait for VM to boot
if err := vm.WaitForSSH(ctx); err != nil {
t.Fatalf("waiting for SSH: %v", err)
}
// Run your actual tests against the VM
conn, err := sql.Open("postgres", vm.ConnectionString())
if err != nil {
t.Fatalf("connecting to postgres: %v", err)
}
defer conn.Close()
// Your migration tests here
}
This gives you true isolation. Each test gets a fresh VM. No container escapes. No shared state.
When to use this
Nested virtualization isn’t free. It adds overhead. Use it when you need:
- Full hardware isolation - Containers share a kernel. VMs don’t.
- Testing hypervisor code - Building tools that manage VMs? Test them properly.
- Running Windows workloads - Some things just need a real Windows VM.
- Security boundaries - Multi-tenant systems benefit from VM-level isolation.
For most Go applications, containers are still the right choice. They’re faster to start, use less memory, and integrate better with modern tooling.
The Go advantage
Go shines here because of its cross-compilation and static binaries. You can build an orchestration tool on your Mac, deploy it to EC2, and it just works. No runtime dependencies. No version conflicts.
Tools like Firecracker (the microVM technology behind AWS Lambda) have Go SDKs. AWS’s nested virtualization support means you can now run Firecracker inside EC2 for testing.
Wrapping up
AWS nested virtualization opens up new possibilities for Go developers building infrastructure tools. The ecosystem has solid libraries for VM management, and Go’s deployment story makes it easy to run anywhere.
If you’re building CI/CD systems or multi-tenant platforms, this is worth exploring. For everyone else, keep using containers—they’re still the simpler choice for most workloads.