Virtual Machines

The vms resource type provisions virtual machines from cloud providers. VMs are ideal for non-Kubernetes workloads, legacy applications, or scenarios requiring full OS access.

Basic Usage

spec:
  resources:
    vms:
      - name: dev-server
        provider: hetzner
        type: cx21
        image: ubuntu-22.04

Schema Reference

vms:
  - name: string              # Required: VM name (supports templating)
    provider: string          # Required: Cloud provider (hetzner, aws, azure)
    type: string              # Required: Instance/server type
    image: string             # Required: OS image
    count: integer            # Optional: Number of VMs to create (default: 1)
    enabled: boolean          # Optional: Enable/disable this VM (default: true)
    region: string            # Optional: Override default region
    sshKeys:                  # Optional: SSH keys to install
      - string                # Key name or fingerprint
    userData: string          # Optional: Cloud-init user data
    volumes:                  # Optional: Attached volumes
      - name: string          # Volume name
        size: string          # Size (e.g., "50Gi")
        mountPath: string     # Mount path in VM
    network:                  # Optional: Network configuration
      privateIp: string       # Static private IP
      publicIp: boolean       # Assign public IP (default: true)
      vpc: string             # VPC/network name
    tags:                     # Optional: Provider-specific tags
      key: value
    dependsOn:                # Optional: Resource dependencies
      - string

Provider-Specific Configuration

Hetzner Cloud

vms:
  - name: server
    provider: hetzner
    type: cx21          # 2 vCPU, 4GB RAM
    image: ubuntu-22.04
    region: eu-central  # Falkenstein, Germany

Hetzner Server Types:

TypevCPURAMDiskUse Case
cx1112GB20GBMinimal workloads
cx2124GB40GBDevelopment
cx3128GB80GBStandard apps
cx41416GB160GBHeavy workloads
cx51832GB240GBLarge applications
ccx1128GB80GBCPU-optimized
ccx21416GB160GBCPU-optimized

Hetzner Images:

  • ubuntu-22.04, ubuntu-20.04
  • debian-12, debian-11
  • centos-stream-9, rocky-9
  • fedora-39

AWS EC2

vms:
  - name: server
    provider: aws
    type: t3.medium    # 2 vCPU, 4GB RAM
    image: ami-ubuntu-22.04
    region: us-east-1

Common EC2 Instance Types:

TypevCPURAMUse Case
t3.micro21GBMinimal
t3.small22GBLight workloads
t3.medium24GBDevelopment
t3.large28GBStandard
m5.large28GBGeneral purpose
c5.large24GBCompute-optimized

Azure VMs

vms:
  - name: server
    provider: azure
    type: Standard_B2s    # 2 vCPU, 4GB RAM
    image: Canonical:0001-com-ubuntu-server-jammy:22_04-lts:latest
    region: eastus
    network:
      resourceGroup: my-rg
      vnet: my-vnet
      subnet: default
      publicIp: true

Cloud-Init Configuration

Use userData to configure VMs at boot time with cloud-init:

Basic Setup

vms:
  - name: dev-server
    provider: hetzner
    type: cx21
    image: ubuntu-22.04
    userData: |
      #cloud-config
      package_update: true
      packages:
        - docker.io
        - git
        - curl

      users:
        - name: developer
          sudo: ALL=(ALL) NOPASSWD:ALL
          shell: /bin/bash
          ssh_authorized_keys:
            - ssh-rsa AAAA...

      runcmd:
        - systemctl enable docker
        - systemctl start docker
        - usermod -aG docker developer

Participant Workstations

spec:
  variables:
    - name: participant_count
      type: integer
      default: 10

  resources:
    vms:
      - name: "workstation-{{ .Index }}"
        count: "{{ .Variables.participant_count }}"
        provider: hetzner
        type: cx21
        image: ubuntu-22.04
        userData: |
          #cloud-config
          hostname: workstation-{{ .Index }}
          
          users:
            - name: participant
              sudo: ALL=(ALL) NOPASSWD:ALL
              shell: /bin/bash
              lock_passwd: false
              # Password: participant123 (change in production!)
              passwd: $6$rounds=4096$...
          
          packages:
            - docker.io
            - kubectl
            - helm
            - git
          
          write_files:
            - path: /home/participant/.kube/config
              permissions: '0600'
              owner: participant:participant
              content: |
                {{ .Resources.clusters.main.kubeconfig | indent 16 }}
          
          runcmd:
            - usermod -aG docker participant
            - chown -R participant:participant /home/participant

Installing Custom Software

vms:
  - name: gitlab-runner
    provider: hetzner
    type: cx31
    image: ubuntu-22.04
    userData: |
      #cloud-config
      package_update: true

      apt:
        sources:
          gitlab-runner:
            source: "deb https://packages.gitlab.com/runner/gitlab-runner/ubuntu/ jammy main"
            keyid: F6403F6544A38863DAA0B6E03F01618A51312F3F

      packages:
        - gitlab-runner
        - docker.io

      write_files:
        - path: /etc/gitlab-runner/config.toml
          content: |
            concurrent = 4
            [[runners]]
              name = "docker-runner"
              url = "{{ .Resources.helm.gitlab.url }}"
              token = "{{ .Resources.secrets.runner-token.value }}"
              executor = "docker"
              [runners.docker]
                image = "alpine:latest"
                privileged = true

      runcmd:
        - systemctl enable gitlab-runner
        - systemctl start gitlab-runner

Per-Participant VMs

Create individual VMs for each participant:

spec:
  variables:
    - name: participant_count
      type: integer
      default: 15
      ui:
        label: "Number of Participants"

  resources:
    vms:
      - name: "participant-{{ .Index }}"
        count: "{{ .Variables.participant_count }}"
        provider: hetzner
        type: cx21
        image: ubuntu-22.04
        userData: |
          #cloud-config
          hostname: participant-{{ .Index }}
          
          users:
            - name: user
              sudo: ALL=(ALL) NOPASSWD:ALL
              shell: /bin/bash

This creates participant-0, participant-1, …, participant-14.

Attached Volumes

Add persistent storage to VMs:

vms:
  - name: database-server
    provider: hetzner
    type: cx31
    image: ubuntu-22.04
    volumes:
      - name: postgres-data
        size: 100Gi
        mountPath: /var/lib/postgresql
      - name: backups
        size: 200Gi
        mountPath: /backups
    userData: |
      #cloud-config
      packages:
        - postgresql-15

      runcmd:
        # Wait for volumes to be mounted
        - while [ ! -d /var/lib/postgresql ]; do sleep 1; done
        - chown postgres:postgres /var/lib/postgresql
        - systemctl enable postgresql
        - systemctl start postgresql

Networking

Private Networks

Connect VMs to private networks:

spec:
  resources:
    vms:
      - name: web-server
        provider: hetzner
        type: cx21
        image: ubuntu-22.04
        network:
          publicIp: true
          privateIp: 10.0.1.10
          vpc: training-network

      - name: database
        provider: hetzner
        type: cx31
        image: ubuntu-22.04
        network:
          publicIp: false        # No public access
          privateIp: 10.0.1.20
          vpc: training-network

Accessing VMs

VMs with public IPs are accessible via:

# SSH directly
ssh user@<vm-public-ip>

# Through Teabar proxy
teactl vm ssh my-env/web-server

VMs without public IPs require jumping through a bastion:

# Use Teabar's built-in proxy
teactl vm ssh my-env/database --jump web-server

VM Outputs

Reference VM outputs in other resources:

spec:
  resources:
    vms:
      - name: app-server
        provider: hetzner
        type: cx21
        image: ubuntu-22.04

    dns:
      - name: app
        type: A
        target: "{{ .Resources.vms.app-server.public_ip }}"

    manifests:
      - name: app-config
        cluster: main
        template: |
          apiVersion: v1
          kind: ConfigMap
          metadata:
            name: app-endpoints
          data:
            app_server_ip: "{{ .Resources.vms.app-server.private_ip }}"
            app_server_hostname: "{{ .Resources.vms.app-server.hostname }}"

Available VM outputs:

OutputDescription
.public_ipPublic IP address
.private_ipPrivate IP address
.hostnameVM hostname
.idProvider-specific VM ID
.statusCurrent status

Best Practices

Use Cloud-Init for Configuration

Always use userData instead of manual configuration:

# Good: Declarative, reproducible
userData: |
  #cloud-config
  packages:
    - nginx
  runcmd:
    - systemctl enable nginx

# Avoid: Manual post-creation steps
# ssh root@server 'apt install nginx'

Minimize Public IPs

Only expose VMs that need public access:

vms:
  # Public: Load balancer/bastion
  - name: bastion
    network:
      publicIp: true

  # Private: Application servers
  - name: app-server
    network:
      publicIp: false
      vpc: internal

Use Appropriate Sizes

Start small and scale up as needed:

# Development
vms:
  - name: dev
    type: cx21    # 2 vCPU, 4GB

# Production training
vms:
  - name: prod
    type: cx41    # 4 vCPU, 16GB

Tag Resources

Use tags for organization and cost tracking:

vms:
  - name: training-server
    provider: hetzner
    type: cx31
    tags:
      environment: "{{ .Environment.Name }}"
      project: kubernetes-training
      owner: devops-team
      ttl: "8h"

Comparison with Kubernetes

ConsiderationVMsKubernetes Pods
Startup time30-60 secondsSeconds
IsolationFull OS isolationContainer isolation
Resource efficiencyLowerHigher
ScalingManual/slowerFast auto-scaling
State managementPersistent by defaultEphemeral by default
Use caseLegacy apps, full OS accessCloud-native apps

Related Resources

ende